Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
6 changes: 4 additions & 2 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
UIViewControllerBasedStatusBarAppearance: false,
NSBluetoothAlwaysUsageDescription:
'Resgrid IC uses Bluetooth to connect to wireless headsets and speaker-microphone accessories for Push-to-Talk audio. For example, when you pair a Bluetooth speaker-mic, pressing its talk button transmits your voice to your department audio channel.',
// Allow the app to open its own custom-scheme deep links (needed for SSO callbacks)
LSApplicationQueriesSchemes: ['resgridic'],
// Allow the app to open its own custom-scheme deep links (needed for SSO callbacks).
// The navigation apps the Navigation App setting can hand directions to are detected with
// canOpenURL, which only answers for declared schemes (src/lib/navigation.ts).
LSApplicationQueriesSchemes: ['resgridic', 'waze', 'comgooglemaps'],
},
entitlements: {
// Required for APNs registration. Previously added by the withForegroundNotifications
Expand Down
30 changes: 30 additions & 0 deletions customManifest.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@ const { withAndroidManifest, AndroidConfig } = require('expo/config-plugins');

const SERVICE_NAME = 'app.notifee.core.ForegroundService';

// Schemes src/lib/navigation.ts probes with Linking.canOpenURL before handing directions to a navigation
// app. Android 11+ answers false for any scheme the manifest does not declare in <queries>, even when the
// app is installed. (iOS declares the same schemes in LSApplicationQueriesSchemes in app.config.ts.)
const NAVIGATION_QUERY_SCHEMES = ['waze'];

/** Adds a VIEW-intent <queries> entry for each navigation scheme the manifest does not declare yet. */
const addNavigationAppQueries = (manifest) => {
manifest.manifest.queries = manifest.manifest.queries || [];
if (manifest.manifest.queries.length === 0) {
manifest.manifest.queries.push({});
}

const declaresScheme = (scheme) => manifest.manifest.queries.some((query) => (query.intent || []).some((intent) => (intent.data || []).some((data) => data?.$?.['android:scheme'] === scheme)));

const queries = manifest.manifest.queries[0];
queries.intent = queries.intent || [];
for (const scheme of NAVIGATION_QUERY_SCHEMES) {
if (!declaresScheme(scheme)) {
queries.intent.push({
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
data: [{ $: { 'android:scheme': scheme } }],
});
}
}
return manifest;
};

const withForegroundService = (config) => {
return withAndroidManifest(config, async (config) => {
const manifest = config.modResults;
Expand All @@ -10,6 +37,8 @@ const withForegroundService = (config) => {
manifest.manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools';
}

addNavigationAppQueries(manifest);

const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);

mainApplication['service'] = mainApplication['service'] || [];
Expand All @@ -35,3 +64,4 @@ const withForegroundService = (config) => {
};

module.exports = withForegroundService;
module.exports.addNavigationAppQueries = addNavigationAppQueries;
2 changes: 1 addition & 1 deletion src/app/(app)/poi/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function PoiDetailScreen() {
return;
}

const success = await openMapsWithDirections(poi.Latitude, poi.Longitude, getPoiSelectionLabel(poi, poiTypesById), userLatitude || undefined, userLongitude || undefined);
const success = await openMapsWithDirections(poi.Latitude, poi.Longitude, getPoiSelectionLabel(poi, poiTypesById), userLatitude || undefined, userLongitude || undefined, poi.Address);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Unhandled rejections from the awaited openMapsWithDirections call can prevent the existing failure feedback from reaching the user. Wrap the call in try/catch and show the existing error toast; this also applies to src/components/contacts/contact-details-extra.tsx:27-27, src/components/contacts/contact-details-extra.tsx:30-30, src/components/contacts/contact-details-sheet.tsx:90-90, src/app/call/tests/[id].test.tsx:834-834, src/app/call/tests/[id].test.tsx:845-845, src/app/call/tests/[id].test.tsx:857-857, src/app/call/tests/[id].test.tsx:868-868, src/app/call/tests/[id].test.tsx:881-881, and src/lib/tests/navigation.test.ts:95-95, src/lib/tests/navigation.test.ts:114-114, src/lib/tests/navigation.test.ts:125-125, src/lib/tests/navigation.test.ts:136-136, src/lib/tests/navigation.test.ts:166-166, src/lib/tests/navigation.test.ts:179-179, src/lib/tests/navigation.test.ts:187-187, src/lib/tests/navigation.test.ts:195-195, src/lib/tests/navigation.test.ts:203-203, src/lib/tests/navigation.test.ts:222-222, src/lib/tests/navigation.test.ts:233-233, src/lib/tests/navigation.test.ts:244-244, src/lib/tests/navigation.test.ts:270-270, src/lib/tests/navigation.test.ts:280-280, src/lib/tests/navigation.test.ts:292-292, src/lib/tests/navigation.test.ts:298-298, src/lib/tests/navigation.test.ts:304-304, src/lib/tests/navigation.test.ts:310-310, src/lib/tests/navigation.test.ts:316-316, src/lib/tests/navigation.test.ts:322-322, src/lib/tests/navigation.test.ts:342-342, src/lib/tests/navigation.test.ts:352-352, src/lib/tests/navigation.test.ts:360-360, src/lib/tests/navigation.test.ts:366-366, src/lib/tests/navigation.test.ts:374-374, src/lib/tests/navigation.test.ts:386-386, src/lib/tests/navigation.test.ts:398-398, src/lib/tests/navigation.test.ts:407-407, src/lib/tests/navigation.test.ts:419-419, src/lib/tests/navigation.test.ts:448-448, src/lib/tests/navigation.test.ts:475-475, src/lib/tests/navigation.test.ts:486-486, src/lib/tests/navigation.test.ts:496-496, src/lib/tests/navigation.test.ts:505-505, src/lib/tests/navigation.test.ts:522-522, src/lib/tests/navigation.test.ts:529-529, and src/lib/tests/navigation.test.ts:538-538.

Kody rule violation: Handle async operations with proper error handling

let success: boolean;
try {
  success = await openMapsWithDirections(poi.Latitude, poi.Longitude, getPoiSelectionLabel(poi, poiTypesById), userLatitude || undefined, userLongitude || undefined, poi.Address);
} catch (error) {
  showToast('error', t('routes.failed_to_open_poi_maps'));
  return;
}
Prompt for LLM

File src/app/(app)/poi/[id].tsx:

Line 58:

Unhandled rejections from the awaited openMapsWithDirections call can prevent the existing failure feedback from reaching the user. Wrap the call in try/catch and show the existing error toast; this also applies to src/components/contacts/contact-details-extra.tsx:27-27, src/components/contacts/contact-details-extra.tsx:30-30, src/components/contacts/contact-details-sheet.tsx:90-90, src/app/call/__tests__/[id].test.tsx:834-834, src/app/call/__tests__/[id].test.tsx:845-845, src/app/call/__tests__/[id].test.tsx:857-857, src/app/call/__tests__/[id].test.tsx:868-868, src/app/call/__tests__/[id].test.tsx:881-881, and src/lib/__tests__/navigation.test.ts:95-95, src/lib/__tests__/navigation.test.ts:114-114, src/lib/__tests__/navigation.test.ts:125-125, src/lib/__tests__/navigation.test.ts:136-136, src/lib/__tests__/navigation.test.ts:166-166, src/lib/__tests__/navigation.test.ts:179-179, src/lib/__tests__/navigation.test.ts:187-187, src/lib/__tests__/navigation.test.ts:195-195, src/lib/__tests__/navigation.test.ts:203-203, src/lib/__tests__/navigation.test.ts:222-222, src/lib/__tests__/navigation.test.ts:233-233, src/lib/__tests__/navigation.test.ts:244-244, src/lib/__tests__/navigation.test.ts:270-270, src/lib/__tests__/navigation.test.ts:280-280, src/lib/__tests__/navigation.test.ts:292-292, src/lib/__tests__/navigation.test.ts:298-298, src/lib/__tests__/navigation.test.ts:304-304, src/lib/__tests__/navigation.test.ts:310-310, src/lib/__tests__/navigation.test.ts:316-316, src/lib/__tests__/navigation.test.ts:322-322, src/lib/__tests__/navigation.test.ts:342-342, src/lib/__tests__/navigation.test.ts:352-352, src/lib/__tests__/navigation.test.ts:360-360, src/lib/__tests__/navigation.test.ts:366-366, src/lib/__tests__/navigation.test.ts:374-374, src/lib/__tests__/navigation.test.ts:386-386, src/lib/__tests__/navigation.test.ts:398-398, src/lib/__tests__/navigation.test.ts:407-407, src/lib/__tests__/navigation.test.ts:419-419, src/lib/__tests__/navigation.test.ts:448-448, src/lib/__tests__/navigation.test.ts:475-475, src/lib/__tests__/navigation.test.ts:486-486, src/lib/__tests__/navigation.test.ts:496-496, src/lib/__tests__/navigation.test.ts:505-505, src/lib/__tests__/navigation.test.ts:522-522, src/lib/__tests__/navigation.test.ts:529-529, and src/lib/__tests__/navigation.test.ts:538-538.

Suggested Code:

let success: boolean;
try {
  success = await openMapsWithDirections(poi.Latitude, poi.Longitude, getPoiSelectionLabel(poi, poiTypesById), userLatitude || undefined, userLongitude || undefined, poi.Address);
} catch (error) {
  showToast('error', t('routes.failed_to_open_poi_maps'));
  return;
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

if (!success) {
showToast('error', t('routes.failed_to_open_poi_maps'));
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/(app)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Item } from '@/components/settings/item';
import { KeepAliveItem } from '@/components/settings/keep-alive-item';
import { LanguageItem } from '@/components/settings/language-item';
import { LoginInfoBottomSheet } from '@/components/settings/login-info-bottom-sheet';
import { NavigationAppItem } from '@/components/settings/navigation-app-item';
import { ServerUrlBottomSheet } from '@/components/settings/server-url-bottom-sheet';
import { ThemeItem } from '@/components/settings/theme-item';
import { ToggleItem } from '@/components/settings/toggle-item';
Expand Down Expand Up @@ -145,6 +146,7 @@ export default function Settings() {
<VStack space="sm">
<ThemeItem />
<LanguageItem />
<NavigationAppItem />
<KeepAliveItem />
<BluetoothDeviceItem />
</VStack>
Expand Down
10 changes: 7 additions & 3 deletions src/app/call/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,14 +271,18 @@ export default function CallDetail() {
* Opens the device's native maps application with directions to the call location
*/
const handleRoute = async () => {
if (coordinates.latitude === null || coordinates.longitude === null) {
// A withheld address must not leave the app in a navigation URL; coordinates still route.
const routableAddress = call?.Address && !isFieldRedacted(call.RedactedFields, ProtectedFieldIds.callAddress, call.Address) ? call.Address.trim() : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Potentially absent RedactedFields and direct call.Address dereferencing can cause errors before isFieldRedacted or trim executes. Use optional chaining and a default value for both properties before passing the data to isFieldRedacted or trim.

Kody rule violation: Add null checks before accessing properties

const address = call?.Address?.trim() ?? ''; const routableAddress = address && !isFieldRedacted(call?.RedactedFields ?? [], ProtectedFieldIds.callAddress, address) ? address : '';
Prompt for LLM

File src/app/call/[id].tsx:

Line 275:

Potentially absent RedactedFields and direct call.Address dereferencing can cause errors before isFieldRedacted or trim executes. Use optional chaining and a default value for both properties before passing the data to isFieldRedacted or trim.

Suggested Code:

const address = call?.Address?.trim() ?? ''; const routableAddress = address && !isFieldRedacted(call?.RedactedFields ?? [], ProtectedFieldIds.callAddress, address) ? address : '';

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Direct dereferencing of potentially null call properties can cause a NullReferenceException. Guard all call properties with optional chaining and null-coalescing before dereferencing them.

Kody rule violation: Add null checks to prevent NullReferenceException

const address = call?.Address?.trim() ?? ''; const routableAddress = address && !isFieldRedacted(call?.RedactedFields ?? [], ProtectedFieldIds.callAddress, address) ? address : '';
Prompt for LLM

File src/app/call/[id].tsx:

Line 275:

Direct dereferencing of potentially null call properties can cause a NullReferenceException. Guard all call properties with optional chaining and null-coalescing before dereferencing them.

Suggested Code:

const address = call?.Address?.trim() ?? ''; const routableAddress = address && !isFieldRedacted(call?.RedactedFields ?? [], ProtectedFieldIds.callAddress, address) ? address : '';

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​


if ((coordinates.latitude === null || coordinates.longitude === null) && !routableAddress) {
showToast('error', t('call_detail.no_location_for_routing'));
return;
}

try {
const destinationName = call?.Address || t('call_detail.call_location');
const success = await openMapsWithDirections(coordinates.latitude, coordinates.longitude, destinationName, userLatitude || undefined, userLongitude || undefined);
const destinationName = routableAddress || t('call_detail.call_location');
// Without usable coordinates the helper navigates to the address instead.
const success = await openMapsWithDirections(coordinates.latitude, coordinates.longitude, destinationName, userLatitude || undefined, userLongitude || undefined, routableAddress);

if (!success) {
showToast('error', t('call_detail.failed_to_open_maps'));
Expand Down
104 changes: 104 additions & 0 deletions src/app/call/__tests__/[id].test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,17 @@ jest.mock('react-i18next', () => ({
}),
}));

// Route hands the destination to the navigation helper, which src/lib/__tests__/navigation.test.ts covers.
const mockOpenMapsWithDirections = jest.fn();
jest.mock('@/lib/navigation', () => ({
openMapsWithDirections: (...args: unknown[]) => mockOpenMapsWithDirections(...args),
}));

// How a withheld value is drawn is ProtectedText's concern; a plain stand-in suits this suite's light UI mocks.
jest.mock('@/components/data-protection/protected-text', () => ({
ProtectedText: ({ value }: { value?: string | null }) => value ?? null,
}));

// React Native mocks
jest.mock('react-native', () => ({
View: jest.fn().mockImplementation(({ children, ...props }) => children),
Expand Down Expand Up @@ -779,4 +790,97 @@ describe('CallDetail', () => {
});
});
});

describe('Route', () => {
const routableCall = {
CallId: 'test-call-id',
Name: 'Test Call',
Number: 'C2024001',
Priority: 2,
Type: 'Emergency',
Address: ' 123 Main St ',
Latitude: '40.7128',
Longitude: '-74.0060',
NotesCount: 0,
ImgagesCount: 0,
FileCount: 0,
};

const mockShowToast = jest.fn();

const setCall = (call: Record<string, unknown>) => {
mockUseCallDetailStore.mockImplementation((selector: any) => {
const store = { call, callExtraData: null, callPriority: null, isLoading: false, error: null, fetchCallDetail: jest.fn(), reset: jest.fn() };
return selector ? selector(store) : store;
});
};

beforeEach(() => {
mockOpenMapsWithDirections.mockReset();
mockOpenMapsWithDirections.mockResolvedValue(true);
mockShowToast.mockReset();
mockUseToastStore.mockImplementation((selector: any) => {
const store = { showToast: mockShowToast };
return selector ? selector(store) : store;
});
});

it('routes to the call coordinates, named by the trimmed address, with the address as the fallback', async () => {
setCall(routableCall);

const { getByTestId } = render(<CallDetail />);
fireEvent.press(getByTestId('button--common.route'));

await waitFor(() => {
expect(mockOpenMapsWithDirections).toHaveBeenCalledWith(40.7128, -74.006, '123 Main St', 40.7128, -74.006, '123 Main St');
});
});

it('routes to the address when the call has no usable coordinates', async () => {
setCall({ ...routableCall, Latitude: '0', Longitude: '0' });

const { getByTestId } = render(<CallDetail />);
fireEvent.press(getByTestId('button--common.route'));

await waitFor(() => {
expect(mockOpenMapsWithDirections).toHaveBeenCalledWith(null, null, '123 Main St', 40.7128, -74.006, '123 Main St');
});
expect(mockShowToast).not.toHaveBeenCalled();
});

it('keeps a withheld address out of the navigation app', async () => {
setCall({ ...routableCall, RedactedFields: ['calls.address'] });

const { getByTestId } = render(<CallDetail />);
fireEvent.press(getByTestId('button--common.route'));

await waitFor(() => {
expect(mockOpenMapsWithDirections).toHaveBeenCalledWith(40.7128, -74.006, 'call_detail.call_location', 40.7128, -74.006, '');
});
});

it('opens nothing and explains why without coordinates or a usable address', async () => {
setCall({ ...routableCall, Latitude: '0', Longitude: '0', RedactedFields: ['calls.address'] });

const { getByTestId } = render(<CallDetail />);
fireEvent.press(getByTestId('button--common.route'));

await waitFor(() => {
expect(mockShowToast).toHaveBeenCalledWith('error', 'call_detail.no_location_for_routing');
});
expect(mockOpenMapsWithDirections).not.toHaveBeenCalled();
});

it('tells the user when no navigation app opened', async () => {
mockOpenMapsWithDirections.mockResolvedValue(false);
setCall(routableCall);

const { getByTestId } = render(<CallDetail />);
fireEvent.press(getByTestId('button--common.route'));

await waitFor(() => {
expect(mockShowToast).toHaveBeenCalledWith('error', 'call_detail.failed_to_open_maps');
});
});
});
});
21 changes: 9 additions & 12 deletions src/components/contacts/contact-details-extra.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { LockIcon } from 'lucide-react-native';
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking, View } from 'react-native';
import { View } from 'react-native';

import { ProtectedRevealBar } from '@/components/data-protection/protected-reveal-bar';
import { HStack } from '@/components/ui/hstack';
import { HomeIcon, MapPinIcon, SettingsIcon } from '@/components/ui/lucide-icons';
import { Pressable } from '@/components/ui/pressable';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
import { mapsUrls } from '@/lib/contacts/format';
import { logger } from '@/lib/logging';
import { parseCoordinates } from '@/lib/contacts/format';
import { openMapsWithAddress, openMapsWithDirections } from '@/lib/navigation';
import { type ContactAddressData, type ContactResultData } from '@/models/v4/contacts/contactResultData';
import { useContactsStore } from '@/stores/contacts/store';

Expand All @@ -20,17 +20,14 @@ interface ContactDetailsExtraProps {
showCustomFields?: boolean;
}

// Opens the navigation app chosen in Settings, which also logs a failure. A "lat,lng" value routes to the point.
const openMaps = async (query: string) => {
const { app, web } = mapsUrls(query);
try {
if (app && (await Linking.canOpenURL(app))) {
await Linking.openURL(app);
return;
}
await Linking.openURL(web);
} catch (error) {
logger.warn({ message: 'Could not open maps for contact address', context: { error } });
const coordinates = parseCoordinates(query);
if (coordinates) {
await openMapsWithDirections(coordinates.latitude, coordinates.longitude, undefined, undefined, undefined, query);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Unhandled failures from the external openMapsWithDirections navigation call leave the application without contextual diagnostics or application-level handling. Wrap the call in try/catch and log the operation, query, and error with logger.warn; this also applies to src/components/contacts/contact-details-extra.tsx:30-30 and src/components/contacts/contact-details-sheet.tsx:90-90.

Kody rule violation: Add try-catch blocks for external calls

try {
  await openMapsWithDirections(coordinates.latitude, coordinates.longitude, undefined, undefined, undefined, query);
} catch (error) {
  logger.warn({ message: 'Could not open map directions', context: { operation: 'openMapsWithDirections', query, error } });
}
Prompt for LLM

File src/components/contacts/contact-details-extra.tsx:

Line 27:

Unhandled failures from the external openMapsWithDirections navigation call leave the application without contextual diagnostics or application-level handling. Wrap the call in try/catch and log the operation, query, and error with logger.warn; this also applies to src/components/contacts/contact-details-extra.tsx:30-30 and src/components/contacts/contact-details-sheet.tsx:90-90.

Suggested Code:

try {
  await openMapsWithDirections(coordinates.latitude, coordinates.longitude, undefined, undefined, undefined, query);
} catch (error) {
  logger.warn({ message: 'Could not open map directions', context: { operation: 'openMapsWithDirections', query, error } });
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

return;
}
await openMapsWithAddress(query);
};

const AddressRow = ({ label, address }: { label: string; address: ContactAddressData }) => {
Expand Down
36 changes: 8 additions & 28 deletions src/components/contacts/contact-details-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { router } from 'expo-router';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking, Platform, ScrollView, useWindowDimensions, View } from 'react-native';
import { Linking, ScrollView, useWindowDimensions, View } from 'react-native';

import { LocationHistoryPanel } from '@/components/calls/location-history-panel';
import { Avatar, AvatarImage } from '@/components/ui/avatar';
Expand All @@ -26,6 +26,7 @@ import {
import { useAnalytics } from '@/hooks/use-analytics';
import { htmlToText, withoutRedacted } from '@/lib/contacts/format';
import { logger } from '@/lib/logging';
import { openMapsWithAddress } from '@/lib/navigation';
import { ContactType } from '@/models/v4/contacts/contactResultData';
import { useContactsStore } from '@/stores/contacts/store';
import { TermKeys, useTerm } from '@/stores/terminology/store';
Expand Down Expand Up @@ -84,8 +85,13 @@ const ContactField: React.FC<ContactFieldProps> = ({ label, value, icon, isLink,
const handlePress = useCallback(async () => {
if (action === 'none' || !value) return;

if (action === 'address') {
// The navigation app chosen in Settings; the helper owns the per-platform URLs, the fallbacks and the failure log.
await openMapsWithAddress(fullAddress || value.toString());
return;
}

let url: string | null = null;
let webFallbackUrl: string | null = null;

switch (action) {
case 'email':
Expand All @@ -94,17 +100,6 @@ const ContactField: React.FC<ContactFieldProps> = ({ label, value, icon, isLink,
case 'phone':
url = `tel:${value.toString().replace(/[^0-9+]/g, '')}`;
break;
case 'address': {
const addressToOpen = fullAddress || value.toString();
const encodedAddress = encodeURIComponent(addressToOpen);
webFallbackUrl = `https://maps.google.com/?q=${encodedAddress}`;
url = Platform.select({
ios: `maps:0,0?q=${encodedAddress}`,
android: `geo:0,0?q=${encodedAddress}`,
default: webFallbackUrl,
});
break;
}
case 'website': {
const websiteUrl = value.toString();
url = websiteUrl.startsWith('http') ? websiteUrl : `https://${websiteUrl}`;
Expand All @@ -117,9 +112,6 @@ const ContactField: React.FC<ContactFieldProps> = ({ label, value, icon, isLink,
const canOpen = await Linking.canOpenURL(url);
if (canOpen) {
await Linking.openURL(url);
} else if (action === 'address' && webFallbackUrl) {
// Fallback to Google Maps web URL for address action
await Linking.openURL(webFallbackUrl);
} else {
logger.warn({
message: 'Cannot open URL',
Expand All @@ -131,18 +123,6 @@ const ContactField: React.FC<ContactFieldProps> = ({ label, value, icon, isLink,
message: 'Failed to open URL from contact details',
context: { error, url, action },
});

// For address action, attempt web fallback on error
if (action === 'address' && webFallbackUrl) {
try {
await Linking.openURL(webFallbackUrl);
} catch (fallbackError) {
logger.error({
message: 'Failed to open fallback Google Maps URL',
context: { error: fallbackError, webFallbackUrl },
});
}
}
}
}
}, [action, value, fullAddress]);
Expand Down
Loading
Loading