From 84007f9725f9cea322ede1b24744094eb513cf62 Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Mon, 6 Apr 2026 11:08:39 -0300 Subject: [PATCH] fix sonar mobile scope and coverage --- apps/mobile/__tests__/lib/api-client.test.ts | 98 ++++++++++++++++ apps/mobile/__tests__/lib/auth-flow.test.ts | 88 ++++++++++++++ .../lib/habit-optimistic-helpers.test.ts | 92 +++++++++++++++ .../lib/habit-request-builders.test.ts | 107 ++++++++++++++++++ .../mobile/__tests__/lib/query-client.test.ts | 74 ++++++++++++ .../mobile/__tests__/lib/secure-store.test.ts | 53 +++++++++ .../__tests__/stores/chat-store.test.ts | 53 +++++++++ apps/mobile/__tests__/stores/ui-store.test.ts | 89 +++++++++++++++ apps/mobile/hooks/use-speech-to-text.ts | 19 ++-- apps/mobile/lib/api-client.ts | 4 +- apps/mobile/lib/providers.tsx | 4 +- apps/mobile/lib/theme.ts | 16 +-- .../orbit-widget/src/OrbitWidgetView.tsx | 2 +- .../orbit-widget/src/OrbitWidgetView.web.tsx | 3 +- apps/mobile/stores/auth-store.ts | 56 +++++---- apps/mobile/vitest.config.ts | 13 +++ .../components/habits/habit-card.test.tsx | 32 +++--- apps/web/components/habits/habit-calendar.tsx | 11 +- apps/web/components/habits/habit-card.tsx | 1 - apps/web/components/ui/app-date-picker.tsx | 11 +- apps/web/hooks/use-speech-to-text.ts | 6 +- sonar-project.properties | 7 +- 22 files changed, 758 insertions(+), 81 deletions(-) create mode 100644 apps/mobile/__tests__/lib/api-client.test.ts create mode 100644 apps/mobile/__tests__/lib/auth-flow.test.ts create mode 100644 apps/mobile/__tests__/lib/habit-optimistic-helpers.test.ts create mode 100644 apps/mobile/__tests__/lib/habit-request-builders.test.ts create mode 100644 apps/mobile/__tests__/lib/query-client.test.ts create mode 100644 apps/mobile/__tests__/lib/secure-store.test.ts create mode 100644 apps/mobile/__tests__/stores/chat-store.test.ts create mode 100644 apps/mobile/__tests__/stores/ui-store.test.ts diff --git a/apps/mobile/__tests__/lib/api-client.test.ts b/apps/mobile/__tests__/lib/api-client.test.ts new file mode 100644 index 000000000..ec51c7e5a --- /dev/null +++ b/apps/mobile/__tests__/lib/api-client.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getTokenMock, clearAllTokensMock, fetchMock } = vi.hoisted(() => ({ + getTokenMock: vi.fn(), + clearAllTokensMock: vi.fn(), + fetchMock: vi.fn(), +})) + +vi.mock('@/lib/secure-store', () => ({ + getToken: getTokenMock, + clearAllTokens: clearAllTokensMock, +})) + +vi.stubGlobal('fetch', fetchMock) + +import { apiClient } from '@/lib/api-client' + +describe('mobile apiClient', () => { + beforeEach(() => { + getTokenMock.mockReset() + clearAllTokensMock.mockReset() + fetchMock.mockReset() + }) + + it('adds auth and JSON headers for standard requests', async () => { + getTokenMock.mockResolvedValue('token-123') + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ ok: true }), + }) + + await expect(apiClient('/api/test', { method: 'POST', body: '{"a":1}' })).resolves.toEqual({ ok: true }) + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.useorbit.org/api/test', + expect.objectContaining({ + method: 'POST', + body: '{"a":1}', + headers: { + Authorization: 'Bearer token-123', + 'Content-Type': 'application/json', + }, + }), + ) + }) + + it('does not force JSON headers for FormData bodies', async () => { + getTokenMock.mockResolvedValue(null) + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ uploaded: true }), + }) + + const body = new FormData() + body.append('file', 'content') + + await apiClient('/upload', { method: 'POST', body }) + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.useorbit.org/upload', + expect.objectContaining({ + headers: {}, + }), + ) + }) + + it('clears tokens and throws on unauthorized responses', async () => { + getTokenMock.mockResolvedValue('token-123') + fetchMock.mockResolvedValue({ ok: false, status: 401 }) + + await expect(apiClient('/secure')).rejects.toThrow('Unauthorized') + expect(clearAllTokensMock).toHaveBeenCalledTimes(1) + }) + + it('prefers backend error payload text when available', async () => { + getTokenMock.mockResolvedValue(null) + fetchMock.mockResolvedValue({ + ok: false, + status: 400, + json: () => Promise.resolve({ message: 'Validation failed' }), + }) + + await expect(apiClient('/broken')).rejects.toThrow('Validation failed') + }) + + it('returns undefined for 204 responses', async () => { + getTokenMock.mockResolvedValue(null) + fetchMock.mockResolvedValue({ + ok: true, + status: 204, + json: () => Promise.resolve({}), + }) + + await expect(apiClient('/empty')).resolves.toBeUndefined() + }) +}) diff --git a/apps/mobile/__tests__/lib/auth-flow.test.ts b/apps/mobile/__tests__/lib/auth-flow.test.ts new file mode 100644 index 000000000..6bde21140 --- /dev/null +++ b/apps/mobile/__tests__/lib/auth-flow.test.ts @@ -0,0 +1,88 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getItemMock, setItemMock, removeItemMock } = vi.hoisted(() => ({ + getItemMock: vi.fn(), + setItemMock: vi.fn(), + removeItemMock: vi.fn(), +})) + +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { + getItem: getItemMock, + setItem: setItemMock, + removeItem: removeItemMock, + }, +})) + +import { + clearStoredAuthReturnUrl, + clearStoredReferralCode, + consumeStoredAuthReturnUrl, + getSafeReturnUrl, + getStoredAuthReturnUrl, + getStoredReferralCode, + isSafeReturnUrl, + isValidReferralCode, + isValidVerificationCode, + markReferralApplied, + storeAuthReturnUrl, + storeReferralCode, +} from '@/lib/auth-flow' + +describe('mobile auth flow helpers', () => { + beforeEach(() => { + getItemMock.mockReset() + setItemMock.mockReset() + removeItemMock.mockReset() + }) + + it('validates referral codes and verification codes', () => { + expect(isValidReferralCode('orbit_123')).toBe(true) + expect(isValidReferralCode('../bad')).toBe(false) + expect(isValidVerificationCode('123456')).toBe(true) + expect(isValidVerificationCode('12345')).toBe(false) + }) + + it('accepts only safe return URLs', () => { + expect(isSafeReturnUrl('/profile')).toBe(true) + expect(isSafeReturnUrl('//evil.com')).toBe(false) + expect(getSafeReturnUrl('//evil.com', '/login')).toBe('/login') + }) + + it('stores and retrieves valid referral codes', async () => { + getItemMock.mockResolvedValue('invite_abc') + + await storeReferralCode('invite_abc') + await expect(getStoredReferralCode()).resolves.toBe('invite_abc') + expect(setItemMock).toHaveBeenCalledWith('referral_code', 'invite_abc') + }) + + it('ignores invalid referral codes', async () => { + getItemMock.mockResolvedValue('bad value') + + await storeReferralCode('bad value') + await expect(getStoredReferralCode()).resolves.toBeNull() + expect(setItemMock).not.toHaveBeenCalled() + }) + + it('stores, consumes, and clears safe return URLs', async () => { + getItemMock.mockResolvedValueOnce('/dashboard') + getItemMock.mockResolvedValueOnce('/dashboard') + + await storeAuthReturnUrl('/dashboard') + await expect(getStoredAuthReturnUrl()).resolves.toBe('/dashboard') + await expect(consumeStoredAuthReturnUrl()).resolves.toBe('/dashboard') + await clearStoredAuthReturnUrl() + + expect(setItemMock).toHaveBeenCalledWith('auth_return_url', '/dashboard') + expect(removeItemMock).toHaveBeenCalledWith('auth_return_url') + }) + + it('clears the stored referral code and marks referral applied', async () => { + await clearStoredReferralCode() + await markReferralApplied() + + expect(removeItemMock).toHaveBeenCalledWith('referral_code') + expect(setItemMock).toHaveBeenCalledWith('orbit_referral_applied', '1') + }) +}) diff --git a/apps/mobile/__tests__/lib/habit-optimistic-helpers.test.ts b/apps/mobile/__tests__/lib/habit-optimistic-helpers.test.ts new file mode 100644 index 000000000..01e31ba60 --- /dev/null +++ b/apps/mobile/__tests__/lib/habit-optimistic-helpers.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' +import { + optimisticToggleCompletion, + optimisticUpdateChecklist, +} from '@/lib/habit-optimistic-helpers' +import type { ChecklistItem, HabitScheduleChild, HabitScheduleItem } from '@orbit/shared/types/habit' + +function makeHabit(overrides: Partial = {}): HabitScheduleItem { + return { + id: 'habit-1', + title: 'Test Habit', + description: '', + frequencyUnit: null, + frequencyQuantity: null, + isBadHabit: false, + isCompleted: false, + isGeneral: false, + isFlexible: false, + days: [], + dueDate: '2025-01-15', + dueTime: '', + dueEndTime: '', + endDate: '', + position: 0, + checklistItems: [], + createdAtUtc: '2025-01-01T00:00:00Z', + parentId: null, + scheduledDates: [], + isOverdue: false, + reminderEnabled: false, + reminderTimes: [], + scheduledReminders: [], + slipAlertEnabled: false, + tags: [], + hasSubHabits: false, + flexibleTarget: null, + flexibleCompleted: 0, + isLoggedInRange: false, + linkedGoals: [], + instances: [], + children: [], + searchMatches: null, + ...overrides, + } as unknown as HabitScheduleItem +} + +describe('mobile optimistic habit helpers', () => { + it('toggles top-level completion and resets recurring checklist items', () => { + const result = optimisticToggleCompletion( + [ + makeHabit({ + frequencyUnit: 'Day', + checklistItems: [ + { text: 'A', isChecked: true }, + { text: 'B', isChecked: true }, + ], + }), + ], + 'habit-1', + ) + + expect(result[0]?.isCompleted).toBe(true) + expect(result[0]?.checklistItems.map((item) => item.isChecked)).toEqual([false, false]) + }) + + it('toggles child completion', () => { + const child = makeHabit({ id: 'child-1' }) + const parent = makeHabit({ + id: 'parent-1', + children: [child] as unknown as HabitScheduleChild[], + }) + + const result = optimisticToggleCompletion([parent], 'child-1') + + expect(result[0]?.children[0]?.isCompleted).toBe(true) + }) + + it('updates checklist items for parent and child habits', () => { + const newItems: ChecklistItem[] = [{ text: 'Done', isChecked: true }] + const child = makeHabit({ id: 'child-1' }) + const parent = makeHabit({ + id: 'parent-1', + children: [child] as unknown as HabitScheduleChild[], + }) + + const topLevelResult = optimisticUpdateChecklist([makeHabit()], 'habit-1', newItems) + const childResult = optimisticUpdateChecklist([parent], 'child-1', newItems) + + expect(topLevelResult[0]?.checklistItems).toEqual(newItems) + expect(childResult[0]?.children[0]?.checklistItems).toEqual(newItems) + }) +}) diff --git a/apps/mobile/__tests__/lib/habit-request-builders.test.ts b/apps/mobile/__tests__/lib/habit-request-builders.test.ts new file mode 100644 index 000000000..14aa29a27 --- /dev/null +++ b/apps/mobile/__tests__/lib/habit-request-builders.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest' +import { + buildCreateHabitRequest, + buildSubHabitRequest, + buildUpdateHabitRequest, + type HabitFormData, +} from '@/lib/habit-request-builders' + +function makeFormData(overrides: Partial = {}): HabitFormData { + return { + title: 'Test Habit', + description: '', + isGeneral: false, + isFlexible: false, + frequencyUnit: null, + frequencyQuantity: null, + days: [], + dueDate: '2025-06-15', + dueTime: '', + dueEndTime: '', + endDate: '', + isBadHabit: false, + slipAlertEnabled: false, + reminderEnabled: false, + scheduledReminders: [], + checklistItems: [], + ...overrides, + } +} + +describe('mobile habit request builders', () => { + it('builds create payloads with recurring fields, reminders, tags, goals, and sub-habits', () => { + const data = makeFormData({ + description: 'Description', + frequencyUnit: 'Week', + frequencyQuantity: 1, + days: ['Monday', 'Wednesday'], + dueTime: '09:00', + dueEndTime: '10:00', + reminderEnabled: true, + checklistItems: [{ text: 'Step 1', isChecked: false }], + }) + + const result = buildCreateHabitRequest(data, [15, 30], ['tag-1'], ['goal-1'], ['Sub 1', '', 'Sub 2']) + + expect(result).toMatchObject({ + title: 'Test Habit', + description: 'Description', + frequencyUnit: 'Week', + frequencyQuantity: 1, + days: ['Monday', 'Wednesday'], + dueTime: '09:00', + dueEndTime: '10:00', + reminderEnabled: true, + reminderTimes: [15, 30], + tagIds: ['tag-1'], + goalIds: ['goal-1'], + subHabits: ['Sub 1', 'Sub 2'], + checklistItems: [{ text: 'Step 1', isChecked: false }], + }) + }) + + it('builds general create payloads without schedule fields', () => { + const result = buildCreateHabitRequest(makeFormData({ isGeneral: true }), [], [], [], []) + expect(result.isGeneral).toBe(true) + expect(result.frequencyUnit).toBeUndefined() + }) + + it('builds sub-habit payloads with schedule and tags', () => { + const result = buildSubHabitRequest( + makeFormData({ frequencyUnit: 'Day', frequencyQuantity: 2 }), + [10], + ['tag-1'], + ) + + expect(result).toMatchObject({ + title: 'Test Habit', + frequencyUnit: 'Day', + frequencyQuantity: 2, + tagIds: ['tag-1'], + }) + }) + + it('builds update payloads including clearEndDate and scheduled reminders', () => { + const result = buildUpdateHabitRequest( + makeFormData({ + frequencyUnit: 'Day', + frequencyQuantity: 1, + reminderEnabled: true, + scheduledReminders: [{ when: 'same_day', time: '08:00' }], + }), + false, + '2025-06-01', + [], + ['goal-1', 'goal-2'], + ) + + expect(result).toMatchObject({ + frequencyUnit: 'Day', + frequencyQuantity: 1, + clearEndDate: true, + reminderEnabled: true, + scheduledReminders: [{ when: 'same_day', time: '08:00' }], + goalIds: ['goal-1', 'goal-2'], + }) + }) +}) diff --git a/apps/mobile/__tests__/lib/query-client.test.ts b/apps/mobile/__tests__/lib/query-client.test.ts new file mode 100644 index 000000000..32daae9b1 --- /dev/null +++ b/apps/mobile/__tests__/lib/query-client.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getItemMock, setItemMock } = vi.hoisted(() => ({ + getItemMock: vi.fn(), + setItemMock: vi.fn(), +})) + +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { + getItem: getItemMock, + setItem: setItemMock, + }, +})) + +import { queryClient, persistQueryCache, restoreQueryCache } from '@/lib/query-client' + +describe('mobile query client', () => { + beforeEach(() => { + getItemMock.mockReset() + setItemMock.mockReset() + queryClient.clear() + }) + + it('uses the expected retry and stale defaults', () => { + const defaults = queryClient.getDefaultOptions() + const retry = defaults.queries?.retry as (failureCount: number, error: Error) => boolean + + expect(defaults.queries?.staleTime).toBe(5 * 60 * 1000) + expect(defaults.queries?.gcTime).toBe(24 * 60 * 60 * 1000) + expect(defaults.mutations?.retry).toBe(false) + expect(defaults.queries?.refetchOnWindowFocus).toBe(false) + expect(defaults.queries?.refetchOnReconnect).toBe(true) + expect(retry(0, new Error('status 401'))).toBe(false) + expect(retry(2, new Error('network'))).toBe(true) + expect(retry(3, new Error('network'))).toBe(false) + }) + + it('persists only successful query results', async () => { + queryClient.setQueryData(['good'], { ok: true }, { updatedAt: 123 }) + + await persistQueryCache() + + expect(setItemMock).toHaveBeenCalledWith( + '@orbit/query-cache', + JSON.stringify([ + { + queryKey: ['good'], + state: { + data: { ok: true }, + dataUpdatedAt: 123, + }, + }, + ]), + ) + }) + + it('restores cached query results', async () => { + getItemMock.mockResolvedValue( + JSON.stringify([ + { + queryKey: ['restored'], + state: { + data: { value: 1 }, + dataUpdatedAt: 456, + }, + }, + ]), + ) + + await restoreQueryCache() + + expect(queryClient.getQueryData(['restored'])).toEqual({ value: 1 }) + }) +}) diff --git a/apps/mobile/__tests__/lib/secure-store.test.ts b/apps/mobile/__tests__/lib/secure-store.test.ts new file mode 100644 index 000000000..eff30ef0d --- /dev/null +++ b/apps/mobile/__tests__/lib/secure-store.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { getItemAsyncMock, setItemAsyncMock, deleteItemAsyncMock } = vi.hoisted(() => ({ + getItemAsyncMock: vi.fn(), + setItemAsyncMock: vi.fn(), + deleteItemAsyncMock: vi.fn(), +})) + +vi.mock('expo-secure-store', () => ({ + getItemAsync: getItemAsyncMock, + setItemAsync: setItemAsyncMock, + deleteItemAsync: deleteItemAsyncMock, +})) + +import { + clearAllTokens, + clearRefreshToken, + clearToken, + getRefreshToken, + getToken, + setRefreshToken, + setToken, +} from '@/lib/secure-store' + +describe('mobile secure store helpers', () => { + beforeEach(() => { + getItemAsyncMock.mockReset() + setItemAsyncMock.mockReset() + deleteItemAsyncMock.mockReset() + }) + + it('reads and writes auth tokens', async () => { + getItemAsyncMock.mockResolvedValueOnce('auth').mockResolvedValueOnce('refresh') + + await setToken('auth') + await setRefreshToken('refresh') + + await expect(getToken()).resolves.toBe('auth') + await expect(getRefreshToken()).resolves.toBe('refresh') + + expect(setItemAsyncMock).toHaveBeenNthCalledWith(1, 'auth_token', 'auth') + expect(setItemAsyncMock).toHaveBeenNthCalledWith(2, 'refresh_token', 'refresh') + }) + + it('clears both tokens', async () => { + await clearToken() + await clearRefreshToken() + await clearAllTokens() + + expect(deleteItemAsyncMock).toHaveBeenCalledWith('auth_token') + expect(deleteItemAsyncMock).toHaveBeenCalledWith('refresh_token') + }) +}) diff --git a/apps/mobile/__tests__/stores/chat-store.test.ts b/apps/mobile/__tests__/stores/chat-store.test.ts new file mode 100644 index 000000000..71d8c018a --- /dev/null +++ b/apps/mobile/__tests__/stores/chat-store.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { useChatStore } from '@/stores/chat-store' + +describe('mobile chat store', () => { + beforeEach(() => { + useChatStore.setState({ + messages: [], + isTyping: false, + isStreaming: false, + }) + }) + + it('appends messages and toggles flags', () => { + const { addMessage, setIsTyping, setIsStreaming } = useChatStore.getState() + + addMessage({ + id: 'message-1', + role: 'ai', + content: 'Hello', + timestamp: new Date('2026-04-06T00:00:00Z'), + }) + setIsTyping(true) + setIsStreaming(true) + + const state = useChatStore.getState() + expect(state.messages).toHaveLength(1) + expect(state.isTyping).toBe(true) + expect(state.isStreaming).toBe(true) + }) + + it('clears the conversation state', () => { + useChatStore.setState({ + messages: [ + { + id: 'message-1', + role: 'ai', + content: 'Hello', + timestamp: new Date('2026-04-06T00:00:00Z'), + }, + ], + isTyping: true, + isStreaming: true, + }) + + useChatStore.getState().clearMessages() + + expect(useChatStore.getState()).toMatchObject({ + messages: [], + isTyping: false, + isStreaming: false, + }) + }) +}) diff --git a/apps/mobile/__tests__/stores/ui-store.test.ts b/apps/mobile/__tests__/stores/ui-store.test.ts new file mode 100644 index 000000000..063a96bd9 --- /dev/null +++ b/apps/mobile/__tests__/stores/ui-store.test.ts @@ -0,0 +1,89 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useUIStore } from '@/stores/ui-store' + +describe('mobile ui store', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-04-06T12:00:00Z')) + useUIStore.setState({ + activeFilters: {}, + selectedDate: '2026-04-06', + activeView: 'today', + streakCelebration: null, + allDoneCelebration: false, + allDoneCelebratedDate: '', + goalCompletedCelebration: null, + isSelectMode: false, + selectedHabitIds: new Set(), + manuallySelectedIds: new Set(), + lastCreatedHabitId: null, + showCreateModal: false, + showCreateGoalModal: false, + searchQuery: '', + }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('merges filters and updates search state', () => { + const { setFilters, setSearchQuery, setActiveView } = useUIStore.getState() + + setFilters({ dateFrom: '2026-04-06' }) + setFilters({ dateTo: '2026-04-06' }) + setSearchQuery('focus') + setActiveView('goals') + + expect(useUIStore.getState()).toMatchObject({ + activeFilters: { dateFrom: '2026-04-06', dateTo: '2026-04-06' }, + searchQuery: 'focus', + activeView: 'goals', + }) + }) + + it('toggles selection mode and cascades descendant selection', () => { + const { toggleSelectMode, toggleSelectionCascade } = useUIStore.getState() + + toggleSelectMode() + toggleSelectionCascade( + 'parent', + () => ['child-1', 'child-2'], + () => false, + ) + + expect(useUIStore.getState().selectedHabitIds).toEqual(new Set(['parent', 'child-1', 'child-2'])) + + toggleSelectionCascade( + 'parent', + () => ['child-1', 'child-2'], + () => false, + ) + + expect(useUIStore.getState().selectedHabitIds.size).toBe(0) + }) + + it('shows all-done celebration only for completed top-level habits on today filters', () => { + useUIStore.setState({ + activeFilters: { dateFrom: '2026-04-06', dateTo: '2026-04-06' }, + }) + + useUIStore.getState().checkAllDoneCelebration( + new Map([ + ['parent-1', { parentId: null, isCompleted: true }], + ['child-1', { parentId: 'parent-1', isCompleted: false }], + ]), + ) + + expect(useUIStore.getState().allDoneCelebration).toBe(true) + }) + + it('clears the last created habit id after the timeout', async () => { + useUIStore.getState().setLastCreatedHabitId('habit-1') + expect(useUIStore.getState().lastCreatedHabitId).toBe('habit-1') + + await vi.advanceTimersByTimeAsync(1500) + + expect(useUIStore.getState().lastCreatedHabitId).toBeNull() + }) +}) diff --git a/apps/mobile/hooks/use-speech-to-text.ts b/apps/mobile/hooks/use-speech-to-text.ts index 4ae372f2a..3ca05c6a4 100644 --- a/apps/mobile/hooks/use-speech-to-text.ts +++ b/apps/mobile/hooks/use-speech-to-text.ts @@ -3,10 +3,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { Platform } from 'react-native' import Constants from 'expo-constants' import { useTranslation } from 'react-i18next' -import { CHAT_SPEECH_LANG_KEY, CHAT_SPEECH_LANGUAGES as SPEECH_LANGUAGES, getDefaultChatSpeechLanguage } from '@orbit/shared/chat' -import { CHAT_VISUALIZER_BAR_OFFSETS as VISUALIZER_BAR_OFFSETS } from '@orbit/shared/chat' - -export { SPEECH_LANGUAGES, VISUALIZER_BAR_OFFSETS } +import { CHAT_SPEECH_LANG_KEY, getDefaultChatSpeechLanguage } from '@orbit/shared/chat' +export { + CHAT_SPEECH_LANGUAGES as SPEECH_LANGUAGES, + CHAT_VISUALIZER_BAR_OFFSETS as VISUALIZER_BAR_OFFSETS, +} from '@orbit/shared/chat' interface SpeechResult { transcript: string @@ -79,7 +80,7 @@ export function useSpeechToText() { const [isSupported, setIsSupported] = useState(() => getSpeechRecognitionAvailability()) const [transcript, setTranscript] = useState('') const [error, setError] = useState(null) - const [selectedLanguage, setSelectedLanguageRaw] = useState(() => + const [selectedLanguage, setSelectedLanguageState] = useState(() => getDefaultChatSpeechLanguage(i18n.language), ) const [recordingDuration, setRecordingDuration] = useState(0) @@ -106,7 +107,7 @@ export function useSpeechToText() { AsyncStorage.getItem(CHAT_SPEECH_LANG_KEY) .then((storedLanguage) => { if (storedLanguage && isMountedRef.current) { - setSelectedLanguageRaw(storedLanguage) + setSelectedLanguageState(storedLanguage) } }) .catch(() => { @@ -152,8 +153,8 @@ export function useSpeechToText() { clearTimer() }) - const setSelectedLanguage = useCallback((newLanguage: string) => { - setSelectedLanguageRaw(newLanguage) + const updateSelectedLanguage = useCallback((newLanguage: string) => { + setSelectedLanguageState(newLanguage) AsyncStorage.setItem(CHAT_SPEECH_LANG_KEY, newLanguage).catch(() => { // Ignore persistence failures and keep the in-memory selection. }) @@ -220,7 +221,7 @@ export function useSpeechToText() { transcript, error, selectedLanguage, - setSelectedLanguage, + setSelectedLanguage: updateSelectedLanguage, startRecording, stopRecording, toggleRecording, diff --git a/apps/mobile/lib/api-client.ts b/apps/mobile/lib/api-client.ts index 7609b8b16..db82758ca 100644 --- a/apps/mobile/lib/api-client.ts +++ b/apps/mobile/lib/api-client.ts @@ -17,9 +17,7 @@ export async function apiClient( options: ApiRequestOptions = {}, ): Promise { const token = await getToken() - const headers: Record = { - ...(options.headers ?? {}), - } + const headers: Record = options.headers ?? {} if (token) { headers['Authorization'] = `Bearer ${token}` } diff --git a/apps/mobile/lib/providers.tsx b/apps/mobile/lib/providers.tsx index 1e8d0c43f..7a03e87b2 100644 --- a/apps/mobile/lib/providers.tsx +++ b/apps/mobile/lib/providers.tsx @@ -11,7 +11,7 @@ interface ProvidersProps { children: ReactNode } -function AuthInitializer({ children }: { children: ReactNode }) { +function AuthInitializer({ children }: Readonly<{ children: ReactNode }>) { const initialize = useAuthStore((s) => s.initialize) const [ready, setReady] = useState(false) const runtimeTheme = getRuntimeTheme() @@ -51,7 +51,7 @@ function AuthInitializer({ children }: { children: ReactNode }) { return {children} } -export function Providers({ children }: ProvidersProps) { +export function Providers({ children }: Readonly) { return ( {children} diff --git a/apps/mobile/lib/theme.ts b/apps/mobile/lib/theme.ts index 62bc858ee..d515d3260 100644 --- a/apps/mobile/lib/theme.ts +++ b/apps/mobile/lib/theme.ts @@ -30,16 +30,16 @@ function withAlpha(color: string, opacity: number, fallback: string): string { if (normalized.length === 3) { const [r, g, b] = normalized.split('') const expanded = `${r}${r}${g}${g}${b}${b}` - const red = parseInt(expanded.slice(0, 2), 16) - const green = parseInt(expanded.slice(2, 4), 16) - const blue = parseInt(expanded.slice(4, 6), 16) + const red = Number.parseInt(expanded.slice(0, 2), 16) + const green = Number.parseInt(expanded.slice(2, 4), 16) + const blue = Number.parseInt(expanded.slice(4, 6), 16) return `rgba(${red}, ${green}, ${blue}, ${opacity})` } if (normalized.length === 6) { - const red = parseInt(normalized.slice(0, 2), 16) - const green = parseInt(normalized.slice(2, 4), 16) - const blue = parseInt(normalized.slice(4, 6), 16) + const red = Number.parseInt(normalized.slice(0, 2), 16) + const green = Number.parseInt(normalized.slice(2, 4), 16) + const blue = Number.parseInt(normalized.slice(4, 6), 16) return `rgba(${red}, ${green}, ${blue}, ${opacity})` } @@ -168,11 +168,11 @@ export function createNav( export const colors = new Proxy({} as ReturnType, { get: (_target, prop) => createColors()[prop as keyof ReturnType], -}) as ReturnType +}) export const nav = new Proxy({} as ReturnType, { get: (_target, prop) => createNav()[prop as keyof ReturnType], -}) as ReturnType +}) // --------------------------------------------------------------------------- // Radius presets diff --git a/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.tsx b/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.tsx index da84f2aa8..573235425 100644 --- a/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.tsx +++ b/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.tsx @@ -6,6 +6,6 @@ import type { OrbitWidgetViewProps } from './OrbitWidget.types'; const NativeView: React.ComponentType = requireNativeView('OrbitWidget'); -export default function OrbitWidgetView(props: OrbitWidgetViewProps) { +export default function OrbitWidgetView(props: Readonly) { return ; } diff --git a/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.web.tsx b/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.web.tsx index a412f0d8d..66e9865f6 100644 --- a/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.web.tsx +++ b/apps/mobile/modules/orbit-widget/src/OrbitWidgetView.web.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import type { OrbitWidgetViewProps } from './OrbitWidget.types'; -export default function OrbitWidgetView(props: OrbitWidgetViewProps) { +export default function OrbitWidgetView(props: Readonly) { return (