Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
91540b1
:sparkles: feat(api): add Google Civic Information API integration
ThatXliner May 27, 2026
6e5e4ce
:sparkles: feat(api): add CA Secretary of State election results client
ThatXliner May 27, 2026
1f35488
:sparkles: feat(api): add Open States API for CA state legislation
ThatXliner May 27, 2026
a734b73
:sparkles: feat(api): add Legistar API for local council legislation
ThatXliner May 27, 2026
d80d9eb
:sparkles: feat(scraper): add VOTE411/LWV voter guide scrapers
ThatXliner May 27, 2026
061a328
:sparkles: feat(scraper): add Santa Clara County ROV scraper
ThatXliner May 27, 2026
a85a334
:memo: docs: add civic data sources setup guide
ThatXliner May 27, 2026
f49a733
:wrench: chore: wire up civic data sources to API and scraper
ThatXliner May 27, 2026
98fbac4
:art: style(api): fix formatting and lint errors in civic clients
ThatXliner May 27, 2026
4e7fc62
feat(expo): add date utilities for election countdown
ThatXliner May 27, 2026
0ec69d6
feat(expo): add useUserAddress hook for address persistence
ThatXliner May 27, 2026
dbcd520
feat(expo): add ElectionBanner component with countdown
ThatXliner May 27, 2026
301a8dd
feat(expo): add MyBallotSection with address input and ballot preview
ThatXliner May 27, 2026
89ad8dc
feat(expo): add KeyDatesSection with horizontal scroll cards
ThatXliner May 27, 2026
882a434
feat(expo): add BallotMeasuresSection for propositions
ThatXliner May 27, 2026
de01878
feat(expo): add CandidatesSection grouped by race
ThatXliner May 27, 2026
62c3db9
feat(expo): add LocalBillsSection with Legistar integration
ThatXliner May 27, 2026
7cbabc2
feat(expo): add LocalElectionsScreen with all sections
ThatXliner May 27, 2026
7a7f5cb
feat(expo): integrate ElectionBanner into Browse tab
ThatXliner May 27, 2026
47ddcf7
fix: resolve typecheck and lint errors in local elections components
ThatXliner May 27, 2026
cfe2173
:art: style(auth): apply lint formatting fixes
ThatXliner May 27, 2026
0726bfc
:sparkles: feat(api): add integrations barrel export
ThatXliner May 27, 2026
558065f
:wrench: chore(scripts): exclude scraper from default dev command
ThatXliner May 29, 2026
ea96179
:wrench: fix(api): add sub-path exports for client-safe modules
ThatXliner May 29, 2026
1d2b114
:bug: fix(expo): import legistar from sub-path to avoid bundling pg
ThatXliner May 29, 2026
86e8920
:wrench: fix(expo): enable package exports in Metro for pnpm compat
ThatXliner May 29, 2026
11116d2
:bug: fix(deps): pin react to 19.0.0 to prevent duplicate copies
ThatXliner May 29, 2026
50c3141
:sparkles: feat(api): add mock data fallback for Civic API
ThatXliner May 29, 2026
75d07ad
:sparkles: feat(api): add mock data fallback for Legistar API
ThatXliner May 29, 2026
54d3c8c
:wrench: chore(db): add seed script with tsx and dotenv-cli
ThatXliner May 29, 2026
c4e7dfd
:pencil2: fix(expo): fix trailing whitespace in expo-env.d.ts
ThatXliner May 29, 2026
9bc0053
add seed
ThatXliner May 29, 2026
d8dacad
:memo: docs: document mock data fallbacks and db seed script
ThatXliner May 29, 2026
977dc26
:recycle: refactor(api): move legistar calls behind tRPC route
ThatXliner May 29, 2026
73abb77
:bug: fix(ui): center chevron vertically in ballot measure cards
ThatXliner May 29, 2026
c74b0b6
:lock: fix(storage): use secure storage for user address
ThatXliner May 29, 2026
1a59d79
:pencil2: fix(ui): use appropriate autoCapitalize for address input
ThatXliner May 29, 2026
ba68e14
:recycle: refactor(elections): fetch voterInfo once in screen, pass a…
ThatXliner May 29, 2026
b467dda
:bug: fix(ui): persist election banner dismissal across sessions
ThatXliner May 29, 2026
6ef7555
:sparkles: feat(feed): show election banner as overlay in feed
ThatXliner May 29, 2026
341e240
:fire: refactor(elections): remove banner dismiss functionality
ThatXliner May 29, 2026
797bdef
Merge pull request #88 from billion-app/feat/local-elections-view
ThatXliner May 29, 2026
62fc1af
:memo: docs(expo): add UI revamp design spec
ThatXliner May 29, 2026
8c0aff1
:lipstick: feat(expo): add surface planes, hairlines, content-type to…
ThatXliner May 29, 2026
5cce162
:sparkles: feat(expo): add content card mapper and date helpers
ThatXliner May 29, 2026
592a2bc
:sparkles: feat(expo): add shared UI component library
ThatXliner May 29, 2026
68383f0
:lipstick: feat(expo): switch to 4-tab blurred tab bar
ThatXliner May 29, 2026
6f58f46
:lipstick: feat(expo): rebuild Browse screen to new design
ThatXliner May 29, 2026
2806001
:lipstick: feat(expo): rebuild Feed as full-height civic cards
ThatXliner May 29, 2026
89b4d03
:lipstick: feat(expo): build Elections ballot tab
ThatXliner May 29, 2026
3866b69
:lipstick: feat(expo): rebuild Article detail to new design
ThatXliner May 29, 2026
7140f37
:lipstick: feat(expo): rebuild Settings hub to new design
ThatXliner May 29, 2026
dfbb25f
:lipstick: feat(expo): rebuild settings sub-screens to new design
ThatXliner May 29, 2026
3663187
:bug: fix(api): fall back to mock civic data when the API returns not…
ThatXliner May 29, 2026
e111fe6
Create claude-design-prompt.md
ThatXliner May 30, 2026
ccc5e7b
delete bro
ThatXliner May 30, 2026
222cc81
:lipstick: feat(expo): add ballot summary and accordion sections to e…
ThatXliner May 30, 2026
de3b6c8
:sparkles: feat(expo): add contest and measure detail screens
ThatXliner May 30, 2026
047357f
Merge pull request #89 from billion-app/revamp-ui-screens
ThatXliner May 30, 2026
c9d433d
:sparkles: feat(db): add user preference, blocked content, and settin…
ThatXliner May 30, 2026
ef785a5
:sparkles: feat(api): add user tRPC router for preferences and settings
ThatXliner May 30, 2026
edda978
:recycle: refactor(expo): wire settings screens to real backend data
ThatXliner May 30, 2026
8f04213
:sparkles: feat(db): add election, contest, candidate, and polling lo…
ThatXliner May 30, 2026
9b20f27
:sparkles: feat(scraper): persist Santa Clara ROV data to database
ThatXliner May 30, 2026
b5934a8
:sparkles: feat(api): add localElections tRPC router
ThatXliner May 30, 2026
05790d3
:sparkles: feat(scraper): fetch and persist bill action history for t…
ThatXliner May 30, 2026
f37b72f
:sparkles: feat: add saved articles with bookmark persistence
ThatXliner May 30, 2026
ddf66de
:sparkles: feat(expo): wire sign out button to authClient.signOut
ThatXliner May 30, 2026
6cdee5c
:construction_worker: fix(ci): use project-relative output-dir for ex…
ThatXliner May 30, 2026
7bd3b71
:rotating_light: fix: resolve lint and typecheck errors across packages
ThatXliner May 30, 2026
e476652
:art: style: auto-format with Prettier
ThatXliner May 30, 2026
4e337eb
:card_file_box: feat(db): add Legistar cache tables and Civic API cac…
ThatXliner May 31, 2026
6a29e15
:zap: feat(api): add DB-backed Legistar cache and expand router
ThatXliner May 31, 2026
48cf416
:zap: feat(api): add DB cache for Google Civic API responses
ThatXliner May 31, 2026
14eed7d
:sparkles: feat(expo): wire feed bookmark to tRPC persistence
ThatXliner May 31, 2026
83d3f74
:sparkles: feat(expo): add upcoming meetings and voting records UI
ThatXliner May 31, 2026
95a9865
:fire: refactor: remove santa-clara-rov scraper and localElections ro…
ThatXliner May 31, 2026
10c5a30
:fire: refactor(api): remove unused CA SOS election client and router
ThatXliner May 31, 2026
148dc9f
:sparkles: feat(api): support multi-state queries in Open States client
ThatXliner May 31, 2026
6626f44
:sparkles: feat(api): add Open States tRPC router
ThatXliner May 31, 2026
9be5194
:fire: refactor(api): remove stale CA SOS exports
ThatXliner May 31, 2026
f64a742
:sparkles: feat(db): add role_description table with seed data
ThatXliner May 31, 2026
5388201
:sparkles: feat(api): add AI generation for civic descriptions
ThatXliner May 31, 2026
8e3ca1f
:sparkles: feat(api): enrich contests with role descriptions
ThatXliner May 31, 2026
af55a2c
:lipstick: feat(expo): display role descriptions and measure summaries
ThatXliner May 31, 2026
d973358
:lipstick: feat(expo): show only contest titles on elections overview
ThatXliner May 31, 2026
211ad17
:lipstick: fix(expo): fix contest card text alignment
ThatXliner May 31, 2026
8d77f59
:bug: fix(deps): sync pnpm-lock with root package.json
ThatXliner May 31, 2026
dc0025a
:sparkles: feat(api): add Vote Smart API client for ballot measures
ThatXliner Jun 1, 2026
e9989d3
:sparkles: feat(api): enrich ballot measures from Vote Smart
ThatXliner Jun 1, 2026
1f7d52e
:lipstick: feat(expo): show AI summary as fallback in measure cards
ThatXliner Jun 1, 2026
db8a2aa
:sparkles: feat(api): add deleteAccount procedure for hard account de…
ThatXliner Jun 1, 2026
4168f3f
:sparkles: feat(expo): wire settings support actions to mailto
ThatXliner Jun 1, 2026
a43bcfb
:sparkles: feat(expo): make edit-profile photo and delete actions work
ThatXliner Jun 1, 2026
e542cb8
:sparkles: feat(api): add feedback persistence and data export
ThatXliner Jun 1, 2026
abe6790
:sparkles: feat(expo): submit feedback to backend instead of mailto
ThatXliner Jun 1, 2026
0b6719d
:sparkles: feat(expo): make privacy toggles and data export real
ThatXliner Jun 1, 2026
010c847
:sparkles: feat(expo): validate display name before saving profile
ThatXliner Jun 1, 2026
32997ba
Merge branch 'main' into feat/settings-implementation
ThatXliner Jun 1, 2026
def3d43
:art: style(format): satisfy CI prettier check
ThatXliner Jun 1, 2026
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ AUTH_DISCORD_SECRET='your_discord_client_secret'
# Expo app API URL (for local development, set to localhost:3000)
EXPO_PUBLIC_API_URL=http://localhost:3000

# PostHog product analytics (optional) — analytics are disabled if the key is unset.
# Project API key + host from https://posthog.com (Project Settings)
EXPO_PUBLIC_POSTHOG_KEY=
EXPO_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

# Google Custom Search API (for article images)
# API Key: https://console.cloud.google.com/apis/credentials
# Search Engine: https://programmablesearchengine.google.com/
Expand Down
3 changes: 3 additions & 0 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
"expo-dev-client": "~5.2.4",
"expo-font": "~13.3.2",
"expo-image": "~2.4.1",
"expo-image-picker": "~16.0.6",
"expo-linear-gradient": "~14.1.5",
Comment on lines 37 to 41
"expo-linking": "~7.1.7",
"expo-location": "~18.1.6",
"expo-network": "~7.1.5",
"expo-router": "~5.1.11",
"expo-secure-store": "~14.2.4",
Expand All @@ -49,6 +51,7 @@
"expo-web-browser": "~14.2.0",
"fuse.js": "^7.1.0",
"nativewind": "5.0.0-preview.3",
"posthog-react-native": "^3.3.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "~0.79.6",
Expand Down
22 changes: 21 additions & 1 deletion apps/expo/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
InriaSerif_700Bold_Italic,
} from "@expo-google-fonts/inria-serif";
import { QueryClientProvider } from "@tanstack/react-query";
import { PostHogProvider } from "posthog-react-native";

import { useTheme } from "~/styles";
import { queryClient } from "~/utils/api";
Expand Down Expand Up @@ -87,7 +88,9 @@ export default function RootLayout() {
void loadFonts();
}, []);

return (
const posthogKey = process.env.EXPO_PUBLIC_POSTHOG_KEY;

const tree = (
<QueryClientProvider client={queryClient}>
<Stack
screenOptions={{
Expand All @@ -100,4 +103,21 @@ export default function RootLayout() {
<StatusBar style="light" />
</QueryClientProvider>
);

if (posthogKey) {
return (
<PostHogProvider
apiKey={posthogKey}
options={{
host:
process.env.EXPO_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com",
}}
autocapture={false}
>
{tree}
</PostHogProvider>
);
}

return tree;
}
114 changes: 97 additions & 17 deletions apps/expo/src/app/settings/edit-profile.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { useState } from "react";
import { StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
import {
Alert,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import * as ImagePicker from "expo-image-picker";
import { useMutation, useQuery } from "@tanstack/react-query";

import { Text } from "~/components/Themed";
import { Avatar, GhostButton, Icon, ScreenShell } from "~/components/ui";
import { colors, fontBody, hair, planes } from "~/styles";
import { queryClient, trpc } from "~/utils/api";
import { authClient } from "~/utils/auth";

function getInitials(name: string): string {
return name
Expand All @@ -22,6 +30,7 @@ export default function EditProfileScreen() {

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [nameError, setNameError] = useState<string | null>(null);
const [synced, setSynced] = useState(false);

if (sessionUser && !synced) {
Expand All @@ -39,14 +48,63 @@ export default function EditProfileScreen() {
},
});

const deleteAccount = useMutation(trpc.user.deleteAccount.mutationOptions());

const handleChangePhoto = async () => {
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!perm.granted) {
Alert.alert(
"Permission needed",
"Allow photo library access to change your profile photo.",
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.5,
base64: true,
});
const asset = result.assets?.[0];
if (!result.canceled && asset?.base64) {
const dataUri = `data:image/jpeg;base64,${asset.base64}`;
updateProfile.mutate({ image: dataUri });
}
Comment on lines +69 to +73
Comment on lines +69 to +73
};

const handleDeleteAccount = () => {
Alert.alert(
"Delete account",
"This permanently deletes your account and all your data. This cannot be undone.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: () =>
deleteAccount.mutate(undefined, {
onSuccess: () => void authClient.signOut(),
}),
},
],
);
};

const fields = [
{ label: "DISPLAY NAME", value: name, set: setName },
{ label: "EMAIL", value: email, set: undefined },
];

const handleSave = () => {
if (name && name !== sessionUser?.name) {
updateProfile.mutate({ name });
const trimmed = name.trim();
if (trimmed.length < 1) {
setNameError("Name can't be empty.");
return;
}
setNameError(null);
if (trimmed && trimmed !== sessionUser?.name) {
updateProfile.mutate({ name: trimmed });
}
};

Expand All @@ -72,27 +130,42 @@ export default function EditProfileScreen() {
label="Change photo"
color={colors.bill}
style={{ marginTop: 10, height: 32 }}
onPress={handleChangePhoto}
/>
</View>

{fields.map((f) => (
<View key={f.label} style={{ marginBottom: 18 }}>
<Text style={s.label}>{f.label}</Text>
<TextInput
style={[s.input, !f.set && { opacity: 0.5 }]}
value={f.value}
onChangeText={f.set}
editable={!!f.set}
placeholderTextColor={colors.textSecondary}
autoCapitalize="none"
/>
</View>
))}
{fields.map((f) => {
const set = f.set;
return (
<View key={f.label} style={{ marginBottom: 18 }}>
<Text style={s.label}>{f.label}</Text>
<TextInput
style={[s.input, !set && { opacity: 0.5 }]}
value={f.value}
onChangeText={
set
? (v) => {
set(v);
setNameError(null);
}
: undefined
}
editable={!!set}
placeholderTextColor={colors.textSecondary}
autoCapitalize="none"
/>
{f.label === "DISPLAY NAME" && nameError && (
<Text style={s.error}>{nameError}</Text>
)}
</View>
);
})}

<GhostButton
label="Delete account"
label={deleteAccount.isPending ? "Deleting…" : "Delete account"}
color={colors.red[500]}
style={{ marginTop: 8, alignSelf: "flex-start" }}
onPress={handleDeleteAccount}
/>
Comment on lines 164 to 169
</ScreenShell>
);
Expand Down Expand Up @@ -133,4 +206,11 @@ const s = StyleSheet.create({
fontFamily: "AlbertSans-Regular",
fontSize: 16,
},
error: {
fontFamily: fontBody.semibold,
fontSize: 12,
color: colors.red[500],
marginTop: 6,
paddingLeft: 4,
},
});
48 changes: 45 additions & 3 deletions apps/expo/src/app/settings/feedback.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { useState } from "react";
import { StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
import {
Alert,
Platform,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import Constants from "expo-constants";
import { useMutation } from "@tanstack/react-query";

import type { IconName } from "~/components/ui";
import { Text } from "~/components/Themed";
import { Icon, Kicker, PrimaryButton, ScreenShell } from "~/components/ui";
import { colors, fontBody, hair, planes } from "~/styles";
import { trpc } from "~/utils/api";

Comment on lines 15 to +18
const CATS: { id: string; label: string; icon: IconName }[] = [
{ id: "bug", label: "Bug report", icon: "flag" },
Expand All @@ -16,6 +26,31 @@ export default function FeedbackScreen() {
const [cat, setCat] = useState("bug");
const [text, setText] = useState("");

const submitFeedback = useMutation(
trpc.user.submitFeedback.mutationOptions(),
);

const handleSubmit = () => {
if (!text.trim()) return;
submitFeedback.mutate(
{
category: cat as "bug" | "idea" | "content",
message: text,
os: Platform.OS,
appVersion: Constants.expoConfig?.version ?? "0.1.1",
},
{
onSuccess: () => {
setText("");
Alert.alert("Thanks!", "Your feedback was sent to the team.");
},
onError: () => {
Alert.alert("Couldn't send", "Please try again.");
},
},
);
};

return (
<ScreenShell title="Send Feedback">
<Text style={s.title}>What&apos;s on your mind?</Text>
Expand Down Expand Up @@ -76,9 +111,16 @@ export default function FeedbackScreen() {
multiline
textAlignVertical="top"
/>
<Text style={s.attached}>App version 2.4.0 attached automatically.</Text>
<Text style={s.attached}>
App version {Constants.expoConfig?.version ?? "0.1.1"} attached
automatically.
</Text>

<PrimaryButton label="Submit feedback" />
<PrimaryButton
label={submitFeedback.isPending ? "Sending…" : "Submit feedback"}
onPress={handleSubmit}
disabled={submitFeedback.isPending || !text.trim()}
/>
</ScreenShell>
);
}
Expand Down
31 changes: 22 additions & 9 deletions apps/expo/src/app/settings/help.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useState } from "react";
import { StyleSheet, TouchableOpacity, View } from "react-native";
import { Linking, StyleSheet, TouchableOpacity, View } from "react-native";

import { Text } from "~/components/Themed";
import { Card, Icon, ScreenShell, SearchInput } from "~/components/ui";
import { colors, fontBody } from "~/styles";

const SUPPORT_EMAIL = "hello@billion.app";

const FAQS = [
{
q: "Where does Billion get its information?",
Expand Down Expand Up @@ -57,14 +59,25 @@ export default function HelpScreen() {
))}
</View>

<Card style={s.contactRow}>
<Icon name="message" size={22} color={colors.bill} />
<View style={{ flex: 1 }}>
<Text style={s.contactTitle}>Still stuck?</Text>
<Text style={s.contactSub}>Reach our team directly</Text>
</View>
<Icon name="chevR" size={18} color="#5B6172" />
</Card>
<TouchableOpacity
activeOpacity={0.8}
onPress={() =>
void Linking.openURL(
`mailto:${SUPPORT_EMAIL}?subject=${encodeURIComponent(
"Billion support request",
)}`,
)
}
Comment on lines +62 to +70
>
<Card style={s.contactRow}>
<Icon name="message" size={22} color={colors.bill} />
<View style={{ flex: 1 }}>
<Text style={s.contactTitle}>Still stuck?</Text>
<Text style={s.contactSub}>Reach our team directly</Text>
</View>
<Icon name="chevR" size={18} color="#5B6172" />
</Card>
</TouchableOpacity>
</ScreenShell>
);
}
Expand Down
Loading