Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a6a4664
feat(settings): add settings screen
Luc1412 Jul 30, 2026
b2333cd
Merge branch 'main' into feat/settings
Luc1412 Jul 30, 2026
f401994
Merge branch 'main' into feat/settings
Luc1412 Jul 30, 2026
2579d9b
update licences
Luc1412 Jul 30, 2026
6165a3d
Implement translations
Luc1412 Jul 30, 2026
498b8ad
wire up haptic settings
Luc1412 Jul 30, 2026
ab3153c
Merge branch 'main' into feat/settings
Luc1412 Jul 30, 2026
a8fe2df
update licences
Luc1412 Jul 30, 2026
b4723f2
sort imports
Luc1412 Jul 30, 2026
ffbe704
Merge branch 'main' into feat/settings
Luc1412 Jul 30, 2026
9e04216
cleanup translations
Luc1412 Jul 30, 2026
791fcb4
Merge branch 'main' into feat/settings
Luc1412 Aug 17, 2026
f956993
update licenses
Luc1412 Aug 17, 2026
4a9698c
Use universal component switch instead of rn switch for MD3 support o…
Luc1412 Aug 17, 2026
e01d0b4
Add icon support for android
Luc1412 Aug 17, 2026
914ac59
Ensure theme selector text font won't be sized too large
Luc1412 Aug 18, 2026
6c41435
Merge branch 'main' into feat/settings
Luc1412 Aug 31, 2026
932b7e0
Update open source lincenses
Luc1412 Aug 31, 2026
305b9d7
fix(settings): respect bottom safe area
Luc1412 Aug 31, 2026
a479b37
update translations
Luc1412 Aug 31, 2026
2d9728d
fix(ci): stabilize license generation
Luc1412 Aug 31, 2026
e1cf798
fix(translations): remove unused German and English messages
Luc1412 Aug 31, 2026
7c619dd
feat(feedback): add native feedback forms
Luc1412 Sep 2, 2026
256063a
Merge remote-tracking branch 'origin/main' into feat/feedback
Luc1412 Sep 2, 2026
a60b4bc
update translations
Luc1412 Sep 2, 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
26 changes: 26 additions & 0 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,32 @@ function RootLayoutContent() {
title: "Open-source licenses",
}}
/>
<Stack.Screen
name="settings/feedback"
options={{
headerTransparent: Platform.OS === "ios" ? true : false,
title: "",
presentation:
Platform.OS === "ios"
? isLiquidGlassAvailable() && osName !== "iPadOS"
? "formSheet"
: "modal"
: "modal",
sheetAllowedDetents: [1],
sheetInitialDetentIndex: 0,
gestureEnabled: false,
contentStyle: {
backgroundColor: isLiquidGlassAvailable()
? "transparent"
: tabBarBackgroundColor,
},
headerBlurEffect: isLiquidGlassAvailable()
? undefined
: colorScheme === "dark"
? "dark"
: "light",
}}
/>
<Stack.Screen
name="settings/license"
options={{
Expand Down
44 changes: 44 additions & 0 deletions src/app/settings/feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FeedbackForm } from "@/components/feedback-form";
import { ThemedView } from "@/components/themed-view";
import { useTheme } from "@/hooks/use-theme";
import ArrowBackSymbol from "@expo/material-symbols/arrow_back.xml";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { useLingui } from "@lingui/react/macro";
import { Stack, useRouter } from "expo-router";
import { Platform, StyleSheet } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

export default function FeedbackScreen() {
const router = useRouter();
const { t } = useLingui();
const theme = useTheme();
return (
<>
<Stack.Header
style={{
backgroundColor:
Platform.OS === "ios" ? theme.transparent : theme.background,
}}
/>
<Stack.Screen.Title>{t`Send feedback`}</Stack.Screen.Title>
<Stack.Toolbar placement="left">
<Stack.Toolbar.Button
icon={process.env.EXPO_OS === "ios" ? "xmark" : ArrowBackSymbol}
onPress={() => router.back()}
/>
</Stack.Toolbar>
<ThemedView
style={styles.sheet}
type={isLiquidGlassAvailable() ? "transparent" : "background"}
>
<SafeAreaView edges={["bottom", "left", "right"]} style={styles.sheet}>
<FeedbackForm onClose={() => router.back()} />
</SafeAreaView>
</ThemedView>
</>
);
}

const styles = StyleSheet.create({
sheet: { flex: 1 },
});
6 changes: 6 additions & 0 deletions src/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ export default function SettingsScreen() {
</Section>

<Section title={t`About`}>
<SettingsRow
icon={{ android: "feedback", ios: "bubble.left.and.bubble.right" }}
label={t`Feedback`}
onPress={() => router.navigate("/settings/feedback")}
/>
<Divider />
<SettingsRow
icon={{ android: "star", ios: "star" }}
label={t`Review eduMFA`}
Expand Down
176 changes: 176 additions & 0 deletions src/components/feedback-form.android.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { FeedbackFormProps } from "@/components/feedback-form.types";
import { Spacing } from "@/constants/theme";
import { useFeedbackForm } from "@/hooks/use-feedback-form";
import CloseSymbol from "@expo/material-symbols/close.xml";
import {
Button,
Host,
Icon,
IconButton,
LazyColumn,
OutlinedTextField,
Row,
SegmentedButton,
SingleChoiceSegmentedButtonRow,
Spacer,
Text,
} from "@expo/ui/jetpack-compose";
import {
fillMaxSize,
fillMaxWidth,
imePadding,
weight,
} from "@expo/ui/jetpack-compose/modifiers";
import { useLingui } from "@lingui/react/macro";
import { StyleSheet } from "react-native";

const FULL_WIDTH = [fillMaxWidth()];
const textStyles = {
body: { typography: "bodyMedium" },
caption: { typography: "bodySmall" },
headline: { typography: "headlineSmall" },
label: { typography: "labelLarge" },
segmentedLabel: { typography: "labelSmall" },
successBody: { typography: "bodyLarge" },
} as const;

export function FeedbackForm({
onClose,
showCloseButton = false,
}: FeedbackFormProps) {
const { t } = useLingui();
const form = useFeedbackForm();

return (
<Host style={styles.host} useViewportSizeMeasurement>
<LazyColumn
contentPadding={{
bottom: Spacing.xl,
end: Spacing.lg,
start: Spacing.lg,
top: Spacing.lg,
}}
horizontalAlignment="start"
modifiers={[fillMaxSize(), imePadding()]}
verticalArrangement={{ spacedBy: Spacing.sm }}
>
{form.submitted ? (
<>
<Text style={textStyles.headline}>
{t`Thank you for your feedback`}
</Text>
<Text style={textStyles.successBody}>
{t`Your feedback was submitted successfully.`}
</Text>
<Button modifiers={FULL_WIDTH} onClick={onClose}>
<Text>{t`Done`}</Text>
</Button>
</>
) : (
<>
{showCloseButton ? (
<Row
horizontalAlignment="center"
modifiers={FULL_WIDTH}
verticalAlignment="center"
>
<Text style={textStyles.headline}>{t`Send feedback`}</Text>
<Spacer modifiers={[weight(1)]} />
<IconButton onClick={onClose}>
<Icon
contentDescription={t`Close`}
size={24}
source={CloseSymbol}
/>
</IconButton>
</Row>
) : null}
<Text style={textStyles.body}>
{t`Tell us what worked well or what we can improve.`}
</Text>

<Text style={textStyles.label}>{t`Feedback type`}</Text>
<SingleChoiceSegmentedButtonRow modifiers={FULL_WIDTH}>
{form.feedbackTypes.map((option) => (
<SegmentedButton
key={option.value}
modifiers={[weight(1)]}
onClick={() => form.setFeedbackType(option.value)}
selected={option.value === form.feedbackType}
>
<SegmentedButton.Label>
<Text maxLines={1} style={textStyles.segmentedLabel}>
{option.label}
</Text>
</SegmentedButton.Label>
</SegmentedButton>
))}
</SingleChoiceSegmentedButtonRow>

<Text style={textStyles.caption}>
{t`Add your name and email if you would like us to contact you with follow-up questions or updates about your feedback.`}
</Text>
<OutlinedTextField
keyboardOptions={{ capitalization: "words", imeAction: "next" }}
modifiers={FULL_WIDTH}
onValueChange={form.setName}
singleLine
>
<OutlinedTextField.Label>
<Text>{t`Name (optional)`}</Text>
</OutlinedTextField.Label>
</OutlinedTextField>
<OutlinedTextField
isError={Boolean(form.emailError)}
keyboardOptions={{ keyboardType: "email", imeAction: "next" }}
modifiers={FULL_WIDTH}
onFocusChanged={(focused) => {
if (!focused) form.validateCurrentEmail();
}}
onValueChange={form.changeEmail}
singleLine
>
<OutlinedTextField.Label>
<Text>{t`Email (optional)`}</Text>
</OutlinedTextField.Label>
{form.emailError ? (
<OutlinedTextField.SupportingText>
<Text>{form.emailError}</Text>
</OutlinedTextField.SupportingText>
) : null}
</OutlinedTextField>
<OutlinedTextField
isError={Boolean(form.messageError || form.submissionError)}
maxLength={2000}
maxLines={4}
minLines={4}
modifiers={FULL_WIDTH}
onValueChange={form.changeMessage}
>
<OutlinedTextField.Label>
<Text>{t`What would you like us to know?`}</Text>
</OutlinedTextField.Label>
{form.messageError || form.submissionError ? (
<OutlinedTextField.SupportingText>
<Text>{form.messageError ?? form.submissionError}</Text>
</OutlinedTextField.SupportingText>
) : null}
</OutlinedTextField>
<Text style={textStyles.caption}>
{t`Your message and optional contact details will be sent to eduMFA through Sentry.`}
</Text>
<Button modifiers={FULL_WIDTH} onClick={form.submit}>
<Text>{t`Submit feedback`}</Text>
</Button>
</>
)}
</LazyColumn>
</Host>
);
}

const styles = StyleSheet.create({
host: {
flex: 1,
},
});
5 changes: 5 additions & 0 deletions src/components/feedback-form.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { FeedbackFormProps } from "@/components/feedback-form.types";

export declare function FeedbackForm(
props: FeedbackFormProps,
): React.JSX.Element;
Loading