Waseel: driver capture, chat/calls, dispatch, and session fixes
Driver onboarding now photographs the licence, ID card and vehicle
registration and reads the credential fields off them, plus a camera-only
profile selfie riders check the arriving driver against. Adds in-app chat
and WebRTC calls, push-backed ride offers, ratings, cancellation and
payment sheets, settlement, and the owner dashboard endpoints behind them.
Camera permission on Android:
- Declare CAMERA and READ_MEDIA_IMAGES in the manifest. expo-image-picker's
own plugin never declares CAMERA, and Android denies a request for an
undeclared permission instantly and silently — no dialog is ever shown,
which is indistinguishable from the app not asking at all.
- Handle canAskAgain: once Android stops showing the dialog, repeating why
we need it is a dead end, so offer Open Settings instead (lib/capture-
permission.ts), matching what the location flow already did.
Session: a 401 on a request that carried a token now ends the session
instead of being reinterpreted per-screen — driver-home had been reading it
as "this user has no driver profile" and showing an onboarding form to an
already-onboarded driver. Requests without a token are exempt so a failed
sign-in doesn't sign you out, and the notification is latched per token so
concurrent polls tear the session down once. (root) gains the auth guard
that turns that into the sign-in screen; app/index.tsx only guarded the way
in, leaving a session that ended mid-screen with nowhere to go.
Also ignore .uploads/ — it holds driver licence, ID and vehicle scans plus
profile photos, which are personal data and must not be committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1d84003e0a
commit
8807ff41c5
+113
-138
@@ -1,8 +1,15 @@
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { Alert, Linking, Platform, ScrollView, Text, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Linking,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Children, Fragment, useCallback, useState } from "react";
|
||||
|
||||
import { SettingsRow } from "@/components/settings-row";
|
||||
import {
|
||||
@@ -12,7 +19,6 @@ import {
|
||||
} from "@/lib/settings";
|
||||
import { useT } from "@/lib/i18n";
|
||||
import { useLocationPermission } from "@/lib/use-location-permission";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
|
||||
type IconName = React.ComponentProps<typeof MaterialCommunityIcons>["name"];
|
||||
|
||||
@@ -22,15 +28,45 @@ const SectionHeader = ({ title }: { title: string }) => (
|
||||
</Text>
|
||||
);
|
||||
|
||||
const Card = ({ children }: { children: React.ReactNode }) => (
|
||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 overflow-hidden shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40">
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
/**
|
||||
* A grouped settings card. Renders an optional muted description header, then
|
||||
* its children with an automatic divider between each row — so callers never
|
||||
* hand-thread `border-t` wrapper Views. Null/conditional children (and arrays
|
||||
* from `.map`) are flattened by `Children.toArray`, so conditionals like
|
||||
* `status !== "granted" ? <Row/> : null` and `options.map(...)` both work.
|
||||
*/
|
||||
const SettingsCard = ({
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const rows = Children.toArray(children);
|
||||
|
||||
return (
|
||||
<View className="rounded-2xl bg-white dark:bg-neutral-900 overflow-hidden shadow-sm shadow-neutral-300 dark:shadow-neutral-950/40">
|
||||
{description ? (
|
||||
<View className="px-4 py-2.5">
|
||||
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{rows.map((row, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 ? (
|
||||
<View className="border-t border-neutral-100 dark:border-neutral-800" />
|
||||
) : null}
|
||||
{row}
|
||||
</Fragment>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const Settings = () => {
|
||||
const t = useT();
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const mode = useSettingsStore((state) => state.mode);
|
||||
const setMode = useSettingsStore((state) => state.setMode);
|
||||
@@ -63,20 +99,6 @@ const Settings = () => {
|
||||
? t("settings.maps.statusBlocked")
|
||||
: t("settings.maps.statusUnknown");
|
||||
|
||||
const modeLabel =
|
||||
mode === "light"
|
||||
? t("settings.appearance.light")
|
||||
: mode === "dark"
|
||||
? t("settings.appearance.dark")
|
||||
: t("settings.appearance.system");
|
||||
|
||||
const langLabel =
|
||||
lang === "en"
|
||||
? t("settings.language.en")
|
||||
: lang === "ar"
|
||||
? t("settings.language.ar")
|
||||
: t("settings.language.fr");
|
||||
|
||||
const callEmergency = useCallback(async () => {
|
||||
try {
|
||||
await Linking.openURL("tel:112");
|
||||
@@ -158,7 +180,7 @@ const Settings = () => {
|
||||
|
||||
{/* 1. Maps & Navigation */}
|
||||
<SectionHeader title={t("settings.maps.title")} />
|
||||
<Card>
|
||||
<SettingsCard>
|
||||
<SettingsRow
|
||||
icon="map-marker-radius"
|
||||
title={t("settings.maps.title")}
|
||||
@@ -167,60 +189,39 @@ const Settings = () => {
|
||||
value={locationStatusLabel}
|
||||
/>
|
||||
{status !== "granted" ? (
|
||||
<View className="border-t border-neutral-100 dark:border-neutral-800">
|
||||
<SettingsRow
|
||||
icon="cog"
|
||||
title={t("settings.maps.openSettings")}
|
||||
right="chevron"
|
||||
onPress={openSettings}
|
||||
/>
|
||||
</View>
|
||||
<SettingsRow
|
||||
icon="cog"
|
||||
title={t("settings.maps.openSettings")}
|
||||
right="chevron"
|
||||
onPress={openSettings}
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 2. Appearance */}
|
||||
<SectionHeader title={t("settings.appearance.title")} />
|
||||
<Card>
|
||||
<View className="px-4 py-2.5">
|
||||
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
|
||||
{t("settings.appearance.description")}
|
||||
</Text>
|
||||
</View>
|
||||
{appearanceOptions.map((option, index) => (
|
||||
<View
|
||||
<SettingsCard description={t("settings.appearance.description")}>
|
||||
{appearanceOptions.map((option) => (
|
||||
<SettingsRow
|
||||
key={option.mode}
|
||||
className={
|
||||
index > 0
|
||||
? "border-t border-neutral-100 dark:border-neutral-800"
|
||||
: ""
|
||||
icon={option.icon}
|
||||
title={
|
||||
option.mode === "light"
|
||||
? t("settings.appearance.light")
|
||||
: option.mode === "dark"
|
||||
? t("settings.appearance.dark")
|
||||
: t("settings.appearance.system")
|
||||
}
|
||||
>
|
||||
<SettingsRow
|
||||
icon={option.icon}
|
||||
title={
|
||||
option.mode === "light"
|
||||
? t("settings.appearance.light")
|
||||
: option.mode === "dark"
|
||||
? t("settings.appearance.dark")
|
||||
: t("settings.appearance.system")
|
||||
}
|
||||
right="value"
|
||||
value={
|
||||
mode === option.mode
|
||||
? isDark
|
||||
? "✓"
|
||||
: "✓"
|
||||
: ""
|
||||
}
|
||||
onPress={() => setMode(option.mode)}
|
||||
/>
|
||||
</View>
|
||||
right="check"
|
||||
selected={mode === option.mode}
|
||||
onPress={() => setMode(option.mode)}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 3. Safety */}
|
||||
<SectionHeader title={t("settings.safety.title")} />
|
||||
<Card>
|
||||
<SettingsCard>
|
||||
<SettingsRow
|
||||
icon="phone-in-talk"
|
||||
title={t("settings.safety.call112")}
|
||||
@@ -230,64 +231,45 @@ const Settings = () => {
|
||||
onPress={callEmergency}
|
||||
/>
|
||||
{safetyTiles.map((tile) => (
|
||||
<View
|
||||
<SettingsRow
|
||||
key={tile.key}
|
||||
className="border-t border-neutral-100 dark:border-neutral-800"
|
||||
>
|
||||
<SettingsRow
|
||||
icon={tile.icon}
|
||||
title={tile.title}
|
||||
subtitle={
|
||||
expandedSafety === tile.key ? undefined : tile.body
|
||||
}
|
||||
right="chevron"
|
||||
onPress={() =>
|
||||
setExpandedSafety((current) =>
|
||||
current === tile.key ? null : tile.key,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
icon={tile.icon}
|
||||
title={tile.title}
|
||||
subtitle={expandedSafety === tile.key ? undefined : tile.body}
|
||||
right="chevron"
|
||||
onPress={() =>
|
||||
setExpandedSafety((current) =>
|
||||
current === tile.key ? null : tile.key,
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 4. Language */}
|
||||
<SectionHeader title={t("settings.language.title")} />
|
||||
<Card>
|
||||
<View className="px-4 py-2.5">
|
||||
<Text className="text-xs font-Jakarta text-general-200 dark:text-neutral-400">
|
||||
{t("settings.language.description")}
|
||||
</Text>
|
||||
</View>
|
||||
{languageOptions.map((option, index) => (
|
||||
<View
|
||||
<SettingsCard description={t("settings.language.description")}>
|
||||
{languageOptions.map((option) => (
|
||||
<SettingsRow
|
||||
key={option.lang}
|
||||
className={
|
||||
index > 0
|
||||
? "border-t border-neutral-100 dark:border-neutral-800"
|
||||
: ""
|
||||
icon={option.icon}
|
||||
title={
|
||||
option.lang === "en"
|
||||
? t("settings.language.en")
|
||||
: option.lang === "ar"
|
||||
? t("settings.language.ar")
|
||||
: t("settings.language.fr")
|
||||
}
|
||||
>
|
||||
<SettingsRow
|
||||
icon={option.icon}
|
||||
title={
|
||||
option.lang === "en"
|
||||
? t("settings.language.en")
|
||||
: option.lang === "ar"
|
||||
? t("settings.language.ar")
|
||||
: t("settings.language.fr")
|
||||
}
|
||||
right="value"
|
||||
value={lang === option.lang ? "✓" : ""}
|
||||
onPress={() => chooseLanguage(option.lang)}
|
||||
/>
|
||||
</View>
|
||||
right="check"
|
||||
selected={lang === option.lang}
|
||||
onPress={() => chooseLanguage(option.lang)}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</SettingsCard>
|
||||
|
||||
{/* 5. Keep awake */}
|
||||
<SectionHeader title={t("settings.keepAwake.title")} />
|
||||
<Card>
|
||||
{/* 5. General — keep-awake toggle + (Android) display-over-other-apps */}
|
||||
<SectionHeader title={t("settings.general.title")} />
|
||||
<SettingsCard>
|
||||
<SettingsRow
|
||||
icon="monitor"
|
||||
title={t("settings.keepAwake.title")}
|
||||
@@ -296,27 +278,20 @@ const Settings = () => {
|
||||
switchValue={keepAwake}
|
||||
onSwitchChange={setKeepAwake}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 6. Display over other apps (Android only) */}
|
||||
{Platform.OS === "android" ? (
|
||||
<>
|
||||
<SectionHeader title={t("settings.overlay.title")} />
|
||||
<Card>
|
||||
<SettingsRow
|
||||
icon="application-brackets"
|
||||
title={t("settings.overlay.allow")}
|
||||
subtitle={
|
||||
overlayRequested
|
||||
? t("settings.overlay.openedHint")
|
||||
: t("settings.overlay.description")
|
||||
}
|
||||
right="chevron"
|
||||
onPress={openOverlaySettings}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
{Platform.OS === "android" ? (
|
||||
<SettingsRow
|
||||
icon="application-brackets"
|
||||
title={t("settings.overlay.allow")}
|
||||
subtitle={
|
||||
overlayRequested
|
||||
? t("settings.overlay.openedHint")
|
||||
: t("settings.overlay.description")
|
||||
}
|
||||
right="chevron"
|
||||
onPress={openOverlaySettings}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsCard>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user