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
Binary file modified .DS_Store
Binary file not shown.
19 changes: 19 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ module.exports = {
'import/no-cycle': 'off', // Disabled due to performance issues
'simple-import-sort/imports': 'error', // Import configuration for `eslint-plugin-simple-import-sort`
'simple-import-sort/exports': 'error', // Export configuration for `eslint-plugin-simple-import-sort`
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'react-native',
importNames: ['Modal'],
message: "Use NativeModal from '@/components/common/native-modal'. A native Modal is its own window, so toasts drawn in the app window are hidden under it; NativeModal carries a toast host inside.",
},
],
},
],
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'off',
'unused-imports/no-unused-vars': [
Expand All @@ -39,6 +51,13 @@ module.exports = {
],
},
overrides: [
{
// The one place React Native's Modal may be used directly.
files: ['src/components/common/native-modal.tsx'],
rules: {
'no-restricted-imports': 'off',
},
},
// Configuration for translations files (i18next)
{
files: ['src/translations/*.json'],
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

| Layer | Technology |
|---|---|
| Framework | React Native 0.81 + Expo SDK 54 (managed, prebuild) |
| Framework | React Native 0.86 + Expo SDK 57 (managed, prebuild) |
| Language | TypeScript (strict mode) |
| Routing | Expo Router (file-based, typed routes) |
| State | Zustand (with MMKV persistence) |
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Some tasks require scanning **every file** -- e.g. "find all dead exports", "lis

| Layer | Technology |
|---|---|
| Framework | React Native 0.81 + Expo SDK 54 (managed, prebuild) |
| Framework | React Native 0.86 + Expo SDK 57 (managed, prebuild) |
| Language | TypeScript (strict mode) |
| Routing | Expo Router (file-based, typed routes) |
| State | Zustand (with MMKV persistence) |
Expand Down
6 changes: 5 additions & 1 deletion app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
},
ios: {
deploymentTarget: '18.1',
// Apps built with the iOS 27 SDK must adopt the UIKit scene life cycle or they are
// killed at launch. Remove once on SDK 58, whose template adopts it by default.
enableSceneSupport: true,
},
},
],
Expand Down Expand Up @@ -264,7 +267,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'expo-navigation-bar',
{
position: 'relative',
visibility: 'hidden',
hidden: true,
behavior: 'inset-touch',
},
],
Expand Down Expand Up @@ -298,6 +301,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
},
],
'./plugins/withInCallAudioModule.js',
'./plugins/withResourceBundleDeploymentTarget.js',
['./plugins/with-app-icon-badge.js', appIconBadgeConfig],
],
extra: {
Expand Down
84 changes: 42 additions & 42 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"@config-plugins/react-native-webrtc": "15.0.1",
"@dev-plugins/react-query": "0.4.0",
"@expo/html-elements": "0.10.1",
"@expo/metro-runtime": "~56.0.20",
"@expo/metro-runtime": "~57.0.16",
"@gluestack-ui/core": "^5.0.15",
"@gluestack-ui/utils": "^5.0.6",
"@gorhom/bottom-sheet": "5.2.13",
Expand All @@ -83,39 +83,39 @@
"buffer": "6.0.3",
"countly-sdk-react-native-bridge": "26.1.0",
"date-fns": "4.1.0",
"expo": "~56.0.20",
"expo-application": "~56.0.3",
"expo-asset": "~56.0.23",
"expo-audio": "~56.0.13",
"expo-auth-session": "~56.0.17",
"expo-build-properties": "~56.0.26",
"expo-clipboard": "~56.0.4",
"expo-constants": "~56.0.24",
"expo-crypto": "~56.0.4",
"expo-dev-client": "~56.0.25",
"expo-device": "~56.0.4",
"expo-document-picker": "~56.0.4",
"expo-file-system": "~56.0.10",
"expo-font": "~56.0.7",
"expo-image": "~56.0.12",
"expo-image-manipulator": "~56.0.25",
"expo-image-picker": "~56.0.24",
"expo-keep-awake": "~56.0.3",
"expo-linking": "~56.0.17",
"expo-localization": "~56.0.6",
"expo-location": "~56.0.24",
"expo-navigation-bar": "~56.0.3",
"expo-notifications": "~56.0.24",
"expo-router": "~56.2.19",
"expo-screen-orientation": "~56.0.5",
"expo-secure-store": "~56.0.4",
"expo-sharing": "~56.0.25",
"expo-splash-screen": "~56.0.14",
"expo-status-bar": "~56.0.4",
"expo-system-ui": "~56.0.5",
"expo-task-manager": "~56.0.26",
"expo-video": "~56.1.4",
"expo-web-browser": "~56.0.6",
"expo": "~57.0.25",
"expo-application": "~57.0.3",
"expo-asset": "~57.0.18",
"expo-audio": "~57.0.5",
"expo-auth-session": "~57.0.13",
"expo-build-properties": "~57.0.22",
"expo-clipboard": "~57.0.2",
"expo-constants": "~57.0.19",
"expo-crypto": "~57.0.3",
"expo-dev-client": "~57.0.19",
"expo-device": "~57.0.2",
"expo-document-picker": "~57.0.2",
"expo-file-system": "~57.0.7",
"expo-font": "~57.0.4",
"expo-image": "~57.0.5",
"expo-image-manipulator": "~57.0.20",
"expo-image-picker": "~57.0.20",
"expo-keep-awake": "~57.0.2",
"expo-linking": "~57.0.11",
"expo-localization": "~57.0.2",
"expo-location": "~57.0.20",
"expo-navigation-bar": "~57.0.2",
"expo-notifications": "~57.0.21",
"expo-router": "~57.0.23",
"expo-screen-orientation": "~57.0.2",
"expo-secure-store": "~57.0.4",
"expo-sharing": "~57.0.22",
"expo-splash-screen": "~57.0.9",
"expo-status-bar": "~57.0.1",
"expo-system-ui": "~57.0.4",
"expo-task-manager": "~57.0.20",
"expo-video": "~57.0.5",
"expo-web-browser": "~57.0.3",
"geojson": "0.5.0",
"i18next": "23.14.0",
"livekit-client": "^2.20.1",
Expand All @@ -131,25 +131,25 @@
"react-error-boundary": "4.0.13",
"react-hook-form": "7.53.2",
"react-i18next": "15.0.3",
"react-native": "0.85.3",
"react-native": "0.86.3",
"react-native-base64": "0.2.2",
"react-native-ble-manager": "12.2.1",
"react-native-callkeep": "github:Irfanwani/react-native-callkeep#957193d0716f1c2dfdc18e627cbff0f8a0800971",
"react-native-css": "^3.0.7",
"react-native-edge-to-edge": "1.6.0",
"react-native-flash-message": "0.4.2",
"react-native-gesture-handler": "~2.31.1",
"react-native-keyboard-controller": "1.21.6",
"react-native-gesture-handler": "~2.32.0",
"react-native-keyboard-controller": "1.21.9",
"react-native-logs": "5.3.0",
"react-native-mmkv": "3.1.0",
"react-native-reanimated": "4.3.1",
"react-native-reanimated": "4.5.1",
"react-native-restart": "0.0.27",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "~4.26.0",
"react-native-svg": "15.15.4",
"react-native-web": "0.21.2",
"react-native-webview": "13.16.1",
"react-native-worklets": "0.8.3",
"react-native-worklets": "0.10.1",
"react-query-kit": "3.3.2",
"sanitize-html": "2.17.0",
"tailwind-variants": "0.2.1",
Expand All @@ -160,8 +160,8 @@
"@babel/core": "^7.29.0",
"@commitlint/cli": "19.2.2",
"@commitlint/config-conventional": "19.2.2",
"@expo/config": "^56.0.0",
"@react-native/jest-preset": "~0.85.3",
"@expo/config": "^57.0.0",
"@react-native/jest-preset": "~0.86.3",
"@tailwindcss/postcss": "^4.2.0",
"@testing-library/jest-dom": "6.5.0",
"@testing-library/react-native": "12.9.0",
Expand Down Expand Up @@ -197,7 +197,7 @@
"eslint-plugin-unused-imports": "2.0.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-expo": "~56.0.5",
"jest-expo": "~57.0.5",
"jest-junit": "16.0.0",
"lint-staged": "15.2.11",
"np": "10.0.7",
Expand Down
28 changes: 28 additions & 0 deletions plugins/__tests__/withResourceBundleDeploymentTarget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { addResourceBundleDeploymentTarget } from '../withResourceBundleDeploymentTarget';

const podfile = `target 'ResgridUnit' do
post_install do |installer|
react_native_post_install(installer)
end
end
`;

describe('addResourceBundleDeploymentTarget', () => {
it('raises resource bundles inside the post_install hook', () => {
const lines = addResourceBundleDeploymentTarget(podfile).split('\n');
const hookLine = lines.findIndex((line) => line.includes('post_install do |installer|'));

expect(lines[hookLine + 1]).toContain('@generated begin resgrid-resource-bundle-deployment-target');
expect(lines.join('\n')).toContain('result.resource_bundle_targets.each');
expect(lines.join('\n')).toContain("build_config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios_version_supported");
expect(lines.findIndex((line) => line.includes('react_native_post_install'))).toBeGreaterThan(hookLine);
});

it('only adds the block once across repeated prebuilds', () => {
const once = addResourceBundleDeploymentTarget(podfile);
const twice = addResourceBundleDeploymentTarget(once);

expect(twice).toBe(once);
expect(twice.match(/resource_bundle_targets/g)).toHaveLength(1);
});
});
43 changes: 43 additions & 0 deletions plugins/withResourceBundleDeploymentTarget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-env node */
const { withPodfile } = require('@expo/config-plugins');
const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode');

/**
* Raises the iOS deployment target of CocoaPods resource-bundle targets to React Native's floor.
*
* `react_native_post_install` lifts every pod's native target to `min_ios_version_supported`, but
* skips the resource bundles that pods such as MapboxMaps, RNSVG and react-native-permissions
* declare. Those keep their podspec minimum (12.4 / 14.0), which Xcode 27 rejects outright — its
* supported range starts at iOS 15.0 — failing the build with "IPHONEOS_DEPLOYMENT_TARGET is set to 12.4".
*/

const POST_INSTALL_BLOCK = ` installer.target_installation_results.pod_target_installation_results.each_value do |result|
result.resource_bundle_targets.each do |bundle_target|
bundle_target.build_configurations.each do |build_config|
deployment_target = build_config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
next if deployment_target.nil? || deployment_target.to_f >= min_ios_version_supported.to_f

build_config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios_version_supported
end
end
end`;

function addResourceBundleDeploymentTarget(podfile) {
return mergeContents({
tag: 'resgrid-resource-bundle-deployment-target',
src: podfile,
newSrc: POST_INSTALL_BLOCK,
anchor: /^\s*post_install do \|installer\|/,
offset: 1,
comment: '#',
}).contents;
}

const withResourceBundleDeploymentTarget = (config) =>
withPodfile(config, (podfileConfig) => {
podfileConfig.modResults.contents = addResourceBundleDeploymentTarget(podfileConfig.modResults.contents);
return podfileConfig;
});

module.exports = withResourceBundleDeploymentTarget;
module.exports.addResourceBundleDeploymentTarget = addResourceBundleDeploymentTarget;
48 changes: 48 additions & 0 deletions src/api/calls/__tests__/callSiteInfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createApiEndpoint } from '../../common/client';
import { getCallSiteInfo } from '../callSiteInfo';

jest.mock('../../common/client', () => {
const get = jest.fn();
return {
createApiEndpoint: jest.fn(() => ({ get })),
__mockGet: get,
};
});

const { __mockGet: mockGet } = jest.requireMock('../../common/client') as { __mockGet: jest.Mock };

// Endpoints are created at module load; capture them before beforeEach clears the mock.
const registeredPaths = (createApiEndpoint as jest.Mock).mock.calls.map((c) => c[0]);

describe('callSiteInfo api', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('targets Calls/GetCallSiteInfo', () => {
expect(registeredPaths).toEqual(['/Calls/GetCallSiteInfo']);
});

it('passes callId as a query parameter and returns the body', async () => {
const payload = { Data: { CallId: '42', IsProtected: false, Contacts: [] } };
mockGet.mockResolvedValueOnce({ data: payload });

await expect(getCallSiteInfo('42')).resolves.toBe(payload);
expect(mockGet).toHaveBeenCalledWith({ callId: '42' }, undefined);
});

it('forwards the abort signal', async () => {
const controller = new AbortController();
mockGet.mockResolvedValueOnce({ data: { Data: null } });

await getCallSiteInfo('42', controller.signal);

expect(mockGet).toHaveBeenCalledWith({ callId: '42' }, controller.signal);
});

it('propagates transport errors', async () => {
mockGet.mockRejectedValueOnce(new Error('Request failed with status code 403'));

await expect(getCallSiteInfo('42')).rejects.toThrow('403');
});
});
15 changes: 15 additions & 0 deletions src/api/calls/callSiteInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type CallSiteInfoResult } from '@/models/v4/calls/callSiteInfoResult';

import { createApiEndpoint } from '../common/client';

// v4 Calls/GetCallSiteInfo (Contacts plan Phase A, decision 3b): every contact linked to a call with its
// pre-plan, premise hazards, live alert notes and site file metadata in one round trip. Needs Call_View.
// The client attaches the Protected Data Grant header automatically; without one, a protected
// department's contact identity, note text and pre-plan text come back REDACTED.

const getCallSiteInfoApi = createApiEndpoint('/Calls/GetCallSiteInfo');

export const getCallSiteInfo = async (callId: string, signal?: AbortSignal) => {
const response = await getCallSiteInfoApi.get<CallSiteInfoResult>({ callId }, signal);
return response.data;
};
12 changes: 12 additions & 0 deletions src/api/calls/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export interface CreateCallRequest {
type?: string;
contactName?: string;
contactInfo?: string;
/** Primary Contact (premises/customer record) to link; the contact must belong to the department. */
contactId?: string | null;
/** Additional Contacts to link. On update, supplying either list replaces the existing links; omitting both leaves them alone. */
additionalContactIds?: string[];
what3words?: string;
plusCode?: string;
dispatchUsers?: string[];
Expand All @@ -71,6 +75,10 @@ export interface UpdateCallRequest {
type?: string;
contactName?: string;
contactInfo?: string;
/** Primary Contact (premises/customer record) to link; the contact must belong to the department. */
contactId?: string | null;
/** Additional Contacts to link. On update, supplying either list replaces the existing links; omitting both leaves them alone. */
additionalContactIds?: string[];
what3words?: string;
plusCode?: string;
dispatchUsers?: string[];
Expand Down Expand Up @@ -126,6 +134,8 @@ export const createCall = async (callData: CreateCallRequest) => {
Type: callData.type || '',
ContactName: callData.contactName || '',
ContactInfo: callData.contactInfo || '',
...(callData.contactId !== undefined ? { ContactId: callData.contactId || '' } : {}),
...(callData.additionalContactIds !== undefined ? { AdditionalContactIds: callData.additionalContactIds } : {}),
What3Words: callData.what3words || '',
PlusCode: callData.plusCode || '',
DispatchList: dispatchList,
Expand Down Expand Up @@ -159,6 +169,8 @@ export const updateCall = async (callData: UpdateCallRequest) => {
Type: callData.type || '',
ContactName: callData.contactName || '',
ContactInfo: callData.contactInfo || '',
...(callData.contactId !== undefined ? { ContactId: callData.contactId || '' } : {}),
...(callData.additionalContactIds !== undefined ? { AdditionalContactIds: callData.additionalContactIds } : {}),
What3Words: callData.what3words || '',
PlusCode: callData.plusCode || '',
DispatchList: dispatchList,
Expand Down
Loading
Loading