Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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