fix(mobile,map): long toasts wrap and http selfhosts get the right geolocation message (#2095)

The phone toast was a single truncated pill, which cut the denied message exactly before the settings hint it exists for; long messages wrap up to four lines now. An insecure context is reported as its own error instead of masquerading as a blocked device permission.
This commit is contained in:
Maurice
2026-09-02 19:00:35 +02:00
committed by Maurice
parent e1af2049f9
commit 088eef6c6b
6 changed files with 129 additions and 9 deletions
+25 -1
View File
@@ -1,4 +1,4 @@
// FE-HOOK-GEO-001 to FE-HOOK-GEO-029
// FE-HOOK-GEO-001 to FE-HOOK-GEO-031
import { StrictMode } from 'react';
import { act, renderHook } from '@testing-library/react';
import { GeoOnceError, getCurrentPositionOnce, useGeolocation } from './useGeolocation';
@@ -354,6 +354,30 @@ describe('useGeolocation', () => {
expect(result.current.position).not.toBeNull();
});
it('FE-HOOK-GEO-030: an insecure origin is named as such, not as a blocked permission', async () => {
// Plain-HTTP self-hosting: the browser would answer PERMISSION_DENIED and the
// user would be sent into device settings that are already correct.
vi.stubGlobal('isSecureContext', false);
const { result } = renderHook(() => useGeolocation());
await act(() => result.current.cycleMode());
expect(watchPosition).not.toHaveBeenCalled();
expect(result.current.mode).toBe('off');
expect(result.current.errorCode).toBe('insecure-context');
});
it('FE-HOOK-GEO-031: a secure origin is not mistaken for an insecure one', async () => {
vi.stubGlobal('isSecureContext', true);
const { result } = renderHook(() => useGeolocation());
await act(() => result.current.cycleMode());
expect(result.current.mode).toBe('show');
expect(result.current.errorCode).toBeNull();
expect(watchPosition).toHaveBeenCalledTimes(1);
});
it('FE-HOOK-GEO-019: stops the watch when the component unmounts', async () => {
const { result, unmount } = renderHook(() => useGeolocation());
await act(() => result.current.cycleMode());
+9 -1
View File
@@ -20,7 +20,7 @@ export type TrackingMode = 'off' | 'show' | 'follow'
// Typed counterpart to the raw error string. The string is whatever the
// browser produced (raw WebKit English on iOS), so the UI keys off this
// code to show a localized, actionable message instead.
export type GeoWatchErrorCode = 'permission-denied' | 'unavailable' | 'timeout' | 'unsupported'
export type GeoWatchErrorCode = 'permission-denied' | 'unavailable' | 'timeout' | 'unsupported' | 'insecure-context'
export interface UseGeolocationReturn {
position: GeoPosition | null
@@ -139,6 +139,14 @@ export function useGeolocation(): UseGeolocationReturn {
setErrorCode('unsupported')
return false
}
// Same block getCurrentPositionOnce guards against: on a plain-HTTP origin
// the browser answers PERMISSION_DENIED, which would otherwise read as a
// revoked device permission and send the user into their OS settings.
if (window.isSecureContext === false) {
setError('Location requires a secure (HTTPS) connection')
setErrorCode('insecure-context')
return false
}
// Already watching, or still waiting on the iOS prompt below: a second
// subscription would overwrite watchIdRef and leave the first one running
// with nobody left holding its id.