Compare commits

...

3 commits

Author SHA1 Message Date
cccb49134f
fix: connect loader 2025-10-23 22:51:43 +02:00
52fc3bc24b
fix(android): phone-call 2025-10-23 22:51:25 +02:00
54083205f0
Revert "fix(up-wip): register stucked loading"
This reverts commit c5ccfa8d08.
2025-10-23 17:20:51 +02:00
7 changed files with 143 additions and 88 deletions

View file

@ -1,20 +1,75 @@
import { Platform, Linking } from "react-native"; import { Platform, Linking, PermissionsAndroid } from "react-native";
import RNImmediatePhoneCall from "react-native-immediate-phone-call"; import RNImmediatePhoneCall from "react-native-immediate-phone-call";
export function phoneCallEmergency() { export async function phoneCallEmergency() {
const emergencyNumber = "112"; const emergencyNumber = "112";
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
// Use telprompt URL scheme on iOS try {
Linking.openURL(`telprompt:${emergencyNumber}`).catch((err) => { // Prefer telprompt on iOS for immediate prompt, fallback to tel
console.error("Error opening phone dialer:", err); await Linking.openURL(`telprompt:${emergencyNumber}`);
// Fallback to regular tel: if telprompt fails } catch (err) {
Linking.openURL(`tel:${emergencyNumber}`).catch((err) => { console.error("Error opening phone dialer (iOS telprompt):", err);
console.error("Error opening phone dialer (fallback):", err); try {
}); await Linking.openURL(`tel:${emergencyNumber}`);
}); } catch (err2) {
} else { console.error("Error opening phone dialer (iOS tel fallback):", err2);
// Use RNImmediatePhoneCall on Android }
}
return;
}
// Android: request CALL_PHONE upfront and provide deterministic fallback
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CALL_PHONE,
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
// Try immediate call, but arm a short fallback timer in case the OS/OEM ignores ACTION_CALL
let fallbackTriggered = false;
const triggerFallback = async () => {
if (fallbackTriggered) return;
fallbackTriggered = true;
try {
await Linking.openURL(`tel:${emergencyNumber}`);
} catch (e) {
console.error("Fallback to dialer failed:", e);
}
};
// Fire fallback after ~1.2s if nothing happens
const timer = setTimeout(triggerFallback, 1200);
try {
RNImmediatePhoneCall.immediatePhoneCall(emergencyNumber); RNImmediatePhoneCall.immediatePhoneCall(emergencyNumber);
} catch (callErr) {
// If native throws synchronously, cancel timer and fallback immediately
clearTimeout(timer);
await triggerFallback();
return;
}
// Give a little extra time; if no fallback was needed, clear the timer
setTimeout(() => {
if (!fallbackTriggered) clearTimeout(timer);
}, 3000);
} else {
// Permission denied or never-ask-again: open dialer prompt
try {
await Linking.openURL(`tel:${emergencyNumber}`);
} catch (err) {
console.error("Permission denied; dialer fallback failed:", err);
}
}
} catch (err) {
console.error("CALL_PHONE permission request failed:", err);
// Last resort: open dialer
try {
await Linking.openURL(`tel:${emergencyNumber}`);
} catch (err2) {
console.error("Dialer fallback failed after permission error:", err2);
}
} }
} }

View file

@ -20,6 +20,7 @@ export default function AccountManagement({
profileData, profileData,
openAccountModal, openAccountModal,
waitingSmsType, waitingSmsType,
clearAuthWaitParams,
}) { }) {
const { colors, custom } = useTheme(); const { colors, custom } = useTheme();
const isConnected = isConnectedProfile(profileData); const isConnected = isConnectedProfile(profileData);
@ -136,6 +137,7 @@ export default function AccountManagement({
modalState={modalState} modalState={modalState}
profileData={profileData} profileData={profileData}
waitingSmsType={waitingSmsType} waitingSmsType={waitingSmsType}
clearAuthWaitParams={clearAuthWaitParams}
/> />
</View> </View>
); );

View file

@ -12,6 +12,7 @@ export default function AccountManagementModal({
modalState, modalState,
profileData, profileData,
waitingSmsType, waitingSmsType,
clearAuthWaitParams,
}) { }) {
const styles = useStyles(); const styles = useStyles();
const [modal, setModal] = modalState; const [modal, setModal] = modalState;
@ -38,6 +39,7 @@ export default function AccountManagementModal({
authMethod={authMethod} authMethod={authMethod}
setAuthMethod={setAuthMethod} setAuthMethod={setAuthMethod}
waitingSmsType={waitingSmsType} waitingSmsType={waitingSmsType}
clearAuthWaitParams={clearAuthWaitParams}
/> />
)} )}
{visible && component === "destroy" && ( {visible && component === "destroy" && (

View file

@ -1,6 +1,6 @@
import React, { useCallback, useState, useEffect } from "react"; import React, { useCallback, useState, useEffect } from "react";
import { View, Alert } from "react-native"; import { View } from "react-native";
import LittleLoader from "~/components/LittleLoader"; import LittleLoader from "~/components/LittleLoader";
import { Button } from "react-native-paper"; import { Button } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
@ -29,6 +29,7 @@ export default function AccountManagementModalConnect({
authMethod, authMethod,
setAuthMethod, setAuthMethod,
waitingSmsType, waitingSmsType,
clearAuthWaitParams,
}) { }) {
const styles = useStyles(); const styles = useStyles();
const { colors, custom } = useTheme(); const { colors, custom } = useTheme();
@ -70,20 +71,12 @@ export default function AccountManagementModalConnect({
}; };
}, [isLoading, loginRequest]); }, [isLoading, loginRequest]);
const connectUsingPhoneNumber = async () => { const connectUsingPhoneNumber = () => {
setIsLoading(true); setIsLoading(true);
try { sendAuthSMS({
await sendAuthSMS({
smsType: "C", smsType: "C",
body: "Se connecter sur Alerte-Secours:\nCode: [CODE]\n💙", // must don't exceed 160 chars including replaced [CODE] body: "Se connecter sur Alerte-Secours:\nCode: [CODE]\n💙", // must don't exceed 160 chars including replaced [CODE]
}); });
} catch (e) {
setIsLoading(false);
Alert.alert(
"Échec de louverture des SMS",
"Impossible douvrir lapplication SMS. Réessayez.",
);
}
}; };
const smsDisclaimerModalStatePair = useState({ visible: false }); const smsDisclaimerModalStatePair = useState({ visible: false });
@ -100,6 +93,7 @@ export default function AccountManagementModalConnect({
const [loginConfirmRequest] = useMutation(LOGIN_CONFIRM_MUTATION); const [loginConfirmRequest] = useMutation(LOGIN_CONFIRM_MUTATION);
const confirmLoginRequest = useCallback(async () => { const confirmLoginRequest = useCallback(async () => {
try {
const deviceUuid = await getDeviceUuid(); const deviceUuid = await getDeviceUuid();
const { const {
data: { data: {
@ -109,11 +103,29 @@ export default function AccountManagementModalConnect({
variables: { loginRequestId: loginRequest.id, deviceUuid }, variables: { loginRequestId: loginRequest.id, deviceUuid },
}); });
await authActions.confirmLoginRequest({ authTokenJwt, isConnected }); await authActions.confirmLoginRequest({ authTokenJwt, isConnected });
}, [loginConfirmRequest, loginRequest?.id, isConnected]); setIsLoading(false);
clearAuthWaitParams?.();
closeModal();
} catch (e) {
setIsLoading(false);
}
}, [
loginConfirmRequest,
loginRequest?.id,
isConnected,
clearAuthWaitParams,
closeModal,
]);
const rejectLoginRequest = useCallback(async () => { const rejectLoginRequest = useCallback(async () => {
try {
await deleteLoginRequest({ variables: { id: loginRequest.id } }); await deleteLoginRequest({ variables: { id: loginRequest.id } });
}, [deleteLoginRequest, loginRequest]); } finally {
setIsLoading(false);
clearAuthWaitParams?.();
closeModal();
}
}, [deleteLoginRequest, loginRequest?.id, clearAuthWaitParams, closeModal]);
return ( return (
<View <View

View file

@ -48,6 +48,7 @@ export default function Form({
profileData, profileData,
openAccountModal, openAccountModal,
waitingSmsType, waitingSmsType,
clearAuthWaitParams,
}) { }) {
const { userId } = useSessionState(["userId"]); const { userId } = useSessionState(["userId"]);
@ -153,7 +154,11 @@ export default function Form({
borderBottomWidth: 1, borderBottomWidth: 1,
}} }}
> >
<PhoneNumbers data={profileData} waitingSmsType={waitingSmsType} /> <PhoneNumbers
data={profileData}
waitingSmsType={waitingSmsType}
clearAuthWaitParams={clearAuthWaitParams}
/>
</View> </View>
<View <View
@ -190,6 +195,7 @@ export default function Form({
profileData={profileData} profileData={profileData}
openAccountModal={openAccountModal} openAccountModal={openAccountModal}
waitingSmsType={waitingSmsType} waitingSmsType={waitingSmsType}
clearAuthWaitParams={clearAuthWaitParams}
/> />
</View> </View>
</View> </View>

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { View, Alert } from "react-native"; import { View } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
@ -26,7 +26,11 @@ import {
import useSendAuthSMS from "~/hooks/useSendAuthSMS"; import useSendAuthSMS from "~/hooks/useSendAuthSMS";
export default function PhoneNumbersView({ data, waitingSmsType }) { export default function PhoneNumbersView({
data,
waitingSmsType,
clearAuthWaitParams,
}) {
const [isLoading, setIsLoading] = useState(waitingSmsType === "R" || false); const [isLoading, setIsLoading] = useState(waitingSmsType === "R" || false);
const phoneNumberList = data.selectOneUser.manyPhoneNumber; const phoneNumberList = data.selectOneUser.manyPhoneNumber;
@ -41,18 +45,10 @@ export default function PhoneNumbersView({ data, waitingSmsType }) {
const registerPhoneNumber = useCallback(async () => { const registerPhoneNumber = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
try { sendAuthSMS({
await sendAuthSMS({
smsType: "R", smsType: "R",
body: "S'enregistrer sur Alerte-Secours:\nCode: [CODE]\n💙", // must don't exceed 160 chars including replaced [CODE] body: "S'enregistrer sur Alerte-Secours:\nCode: [CODE]\n💙", // must don't exceed 160 chars including replaced [CODE]
}); });
} catch (e) {
setIsLoading(false);
Alert.alert(
"Échec de louverture des SMS",
"Impossible douvrir lapplication SMS. Réessayez.",
);
}
}, [sendAuthSMS, setIsLoading]); }, [sendAuthSMS, setIsLoading]);
// Clear loading state after 3 minutes // Clear loading state after 3 minutes
@ -77,8 +73,16 @@ export default function PhoneNumbersView({ data, waitingSmsType }) {
useEffect(() => { useEffect(() => {
if (data.selectOneUser.oneUserLoginRequest) { if (data.selectOneUser.oneUserLoginRequest) {
setIsLoading(false); setIsLoading(false);
clearAuthWaitParams?.();
} }
}, [data.selectOneUser.oneUserLoginRequest]); }, [data.selectOneUser.oneUserLoginRequest, clearAuthWaitParams]);
// Defensive cleanup on unmount to ensure no lingering loader
useEffect(() => {
return () => {
setIsLoading(false);
};
}, []);
const deletePhoneNumberModalStatePair = useState({ visible: false }); const deletePhoneNumberModalStatePair = useState({ visible: false });
const [deletePhoneNumberModalState, setDeletePhoneNumberModalState] = const [deletePhoneNumberModalState, setDeletePhoneNumberModalState] =

View file

@ -1,6 +1,6 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { ScrollView, View, AppState } from "react-native"; import { ScrollView, View } from "react-native";
import Loader from "~/components/Loader"; import Loader from "~/components/Loader";
import { useSubscription } from "@apollo/client"; import { useSubscription } from "@apollo/client";
@ -12,7 +12,6 @@ import { createLogger } from "~/lib/logger";
import { FEATURE_SCOPES } from "~/lib/logger/scopes"; import { FEATURE_SCOPES } from "~/lib/logger/scopes";
import withConnectivity from "~/hoc/withConnectivity"; import withConnectivity from "~/hoc/withConnectivity";
import { useFocusEffect } from "@react-navigation/native";
import Form from "./Form"; import Form from "./Form";
@ -38,38 +37,12 @@ export default withConnectivity(function Profile({ navigation, route }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]); }, [userId]);
useFocusEffect( const clearAuthWaitParams = React.useCallback(() => {
React.useCallback(() => {
restart();
}, [restart]),
);
useEffect(() => {
const sub = AppState.addEventListener("change", (state) => {
if (state === "active") {
restart();
}
});
return () => {
sub?.remove?.();
};
}, [restart]);
useEffect(() => {
if (
route.params?.waitingSmsType &&
data?.selectOneUser?.oneUserLoginRequest
) {
navigation.setParams({ navigation.setParams({
waitingSmsType: undefined, waitingSmsType: undefined,
openAccountModal: true, openAccountModal: undefined,
}); });
} }, [navigation]);
}, [
route.params?.waitingSmsType,
data?.selectOneUser?.oneUserLoginRequest,
navigation,
]);
if (loading || !data?.selectOneUser) { if (loading || !data?.selectOneUser) {
return <Loader />; return <Loader />;
@ -87,6 +60,7 @@ export default withConnectivity(function Profile({ navigation, route }) {
profileData={data} profileData={data}
openAccountModal={route.params?.openAccountModal} openAccountModal={route.params?.openAccountModal}
waitingSmsType={route.params?.waitingSmsType} waitingSmsType={route.params?.waitingSmsType}
clearAuthWaitParams={clearAuthWaitParams}
/> />
</ScrollView> </ScrollView>
); );