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
3 changes: 3 additions & 0 deletions .expo/devices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"devices": []
}
9 changes: 5 additions & 4 deletions apps/mobile/v1/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "Typelets",
"slug": "typelets",
"version": "1.30.12",
"version": "1.30.13",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "typelets",
Expand All @@ -15,7 +15,7 @@
"ios": {
"icon": "./assets/images/ios-icon-dark.png",
"bundleIdentifier": "com.typelets.mobile.ios",
"buildNumber": "59",
"buildNumber": "60",
"supportsTablet": true,
"infoPlist": {
"NSCameraUsageDescription": "This app uses the camera to capture photos for your notes.",
Expand All @@ -26,7 +26,7 @@
},
"android": {
"package": "com.typelets.notes",
"versionCode": 59,
"versionCode": 60,
"softwareKeyboardLayoutMode": "resize",
"adaptiveIcon": {
"backgroundColor": "#FFFFFF",
Expand Down Expand Up @@ -63,7 +63,8 @@
"backgroundColor": "#25262b"
}
}
]
],
"expo-sqlite"
],
"experiments": {
"typedRoutes": true
Expand Down
14 changes: 14 additions & 0 deletions apps/mobile/v1/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { DarkTheme, DefaultTheme, ThemeProvider as NavigationThemeProvider } fro
import * as Sentry from '@sentry/react-native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import { View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

import { AppWrapper } from '@/src/components/AppWrapper';
import ErrorBoundary from '@/src/components/ErrorBoundary';
import { initializeDatabase } from '@/src/lib/database';
import { ThemeProvider, useTheme } from '@/src/theme';


Expand Down Expand Up @@ -107,6 +109,18 @@ export default Sentry.wrap(function RootLayout() {
console.log('Clerk key loaded:', clerkPublishableKey ? 'YES' : 'NO');
}

// Initialize SQLite database on app start
// Works in Expo Go and custom builds!
useEffect(() => {
initializeDatabase()
.then(() => {
console.log('[App] ✅ SQLite database initialized - offline caching enabled');
})
.catch((error) => {
console.error('[App] ❌ Failed to initialize SQLite database:', error);
});
}, []);

// If no Clerk key, show error
if (!clerkPublishableKey) {
if (__DEV__) {
Expand Down
54 changes: 40 additions & 14 deletions apps/mobile/v1/app/folder-notes.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useLocalSearchParams, useRouter, Stack } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { TouchableOpacity, View, StyleSheet, Text, Animated, Alert, Keyboard, Pressable } from 'react-native';
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView, BottomSheetTextInput,BottomSheetView } from '@gorhom/bottom-sheet';
import * as Haptics from 'expo-haptics';
import { Stack,useLocalSearchParams, useRouter } from 'expo-router';
import { useCallback,useEffect, useMemo, useRef, useState } from 'react';
import { Alert, Animated, Keyboard, Pressable,StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTheme } from '@/src/theme';
import NotesListScreen from '@/src/screens/NotesListScreen';
import { useApiService, type Folder } from '@/src/services/api';
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';

import { OfflineIndicator } from '@/src/components/OfflineIndicator';
import { Input } from '@/src/components/ui/Input';
import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView, BottomSheetTextInput } from '@gorhom/bottom-sheet';
import { FOLDER_COLORS } from '@/src/constants/ui';
import NotesListScreen from '@/src/screens/ListNotes';
import { type Folder,useApiService } from '@/src/services/api';
import { useTheme } from '@/src/theme';

function getViewTitle(viewType: string): string {
switch (viewType) {
Expand Down Expand Up @@ -81,12 +83,29 @@ export default function FolderNotesScreen() {
// Build breadcrumbs by traversing up the folder hierarchy
useEffect(() => {
const buildBreadcrumbs = async () => {
if (!params.folderId) {
if (!params.folderId && !params.viewType) {
setBreadcrumbs(['Notes']);
// Still fetch folders for navigation menu
try {
const folders = await api.getFolders();
setAllFolders(folders);
} catch (error) {
console.error('Failed to fetch folders:', error);
setAllFolders([]);
}
return;
}

if (params.viewType) {
// For special views, just show the view name
if (params.viewType) {
setBreadcrumbs([getViewTitle(params.viewType as string)]);
} else {
setBreadcrumbs(['Notes']);
setBreadcrumbs([getViewTitle(params.viewType as string)]);
// Still fetch folders for navigation menu
try {
const folders = await api.getFolders();
setAllFolders(folders);
} catch (error) {
console.error('Failed to fetch folders:', error);
setAllFolders([]);
}
return;
}
Expand Down Expand Up @@ -254,7 +273,7 @@ export default function FolderNotesScreen() {
params: {
folderId: params.folderId as string,
folderName: params.folderName as string,
viewType: params.viewType as string,
viewType: params.viewType as 'all' | 'starred' | 'archived' | 'trash' | undefined,
searchQuery: searchQuery, // Pass search query to NotesListScreen
}
};
Expand Down Expand Up @@ -398,6 +417,9 @@ export default function FolderNotesScreen() {
}
}
}} activeTab="add" /> */}

{/* Offline Indicator - Floating Button */}
<OfflineIndicator />
</SafeAreaView>
</Pressable>

Expand Down Expand Up @@ -445,7 +467,11 @@ export default function FolderNotesScreen() {
{/* Render folder tree hierarchically */}
{(() => {
const renderFolderTree = (parentId: string | null | undefined, depth: number = 0) => {
const folders = allFolders.filter(f => f.parentId === parentId);
// Handle both null and undefined for root folders
const folders = allFolders.filter(f =>
parentId ? f.parentId === parentId : !f.parentId
);

return folders.map((folder) => {
const isInBreadcrumb = breadcrumbFolders.some(bf => bf.id === folder.id);
const isCurrent = breadcrumbFolders[breadcrumbFolders.length - 1]?.id === folder.id;
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/v1/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"developmentClient": true,
"distribution": "internal",
"env": {
"RCT_NEW_ARCH_ENABLED": "1"
"RCT_NEW_ARCH_ENABLED": "0"
}
},
"preview": {
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/v1/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "v1",
"main": "index.js",
"version": "1.30.12",
"version": "1.30.13",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
Expand All @@ -16,6 +16,7 @@
"@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.2.6",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@sentry/react-native": "~7.2.0",
Expand All @@ -33,6 +34,7 @@
"expo-secure-store": "^15.0.7",
"expo-sharing": "~14.0.7",
"expo-splash-screen": "~31.0.10",
"expo-sqlite": "~16.0.8",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
Expand All @@ -53,6 +55,7 @@
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"expo-build-properties": "^1.0.9",
"typescript": "~5.9.2"
},
"private": true
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/v1/src/components/AppWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ActivityIndicator,View } from 'react-native';
import { useMasterPassword } from '../hooks/useMasterPassword';
import { logger } from '../lib/logger';
import AuthScreen from '../screens/AuthScreen';
import { useSyncOnReconnect } from '../services/sync/useSyncOnReconnect';
import { useTheme } from '../theme';
import { MasterPasswordScreen } from './MasterPasswordDialog';

Expand All @@ -21,13 +22,18 @@ export const AppWrapper: React.FC<AppWrapperProps> = ({ children }) => {
isNewSetup,
isChecking,
userId,
loadingStage,
cacheMode,
onPasswordSuccess,
} = useMasterPassword();

const [showLoading, setShowLoading] = useState(false);
const lastUserIdRef = useRef<string | undefined>(undefined);
const [userChanging, setUserChanging] = useState(false);

// Automatically sync pending mutations when device comes back online
useSyncOnReconnect();

// Detect userId change SYNCHRONOUSLY in render
if (userId !== lastUserIdRef.current) {
lastUserIdRef.current = userId;
Expand Down Expand Up @@ -120,6 +126,8 @@ export const AppWrapper: React.FC<AppWrapperProps> = ({ children }) => {
key={userId} // Force remount when userId changes to reset all state
userId={userId || ''}
isNewSetup={isNewSetup}
loadingStage={loadingStage}
cacheMode={cacheMode}
onSuccess={onPasswordSuccess}
/>
);
Expand Down
62 changes: 55 additions & 7 deletions apps/mobile/v1/src/components/MasterPasswordDialog/LoadingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { styles } from './styles';

interface LoadingViewProps {
isNewSetup: boolean;
stage?: 'securing' | 'caching';
cacheMode?: 'encrypted' | 'decrypted';
}

/**
* Loading view component
* Shows during PBKDF2 key derivation with progress indicator
* Shows during PBKDF2 key derivation and note caching
*/
export function LoadingView({ isNewSetup }: LoadingViewProps) {
export function LoadingView({ isNewSetup, stage = 'securing', cacheMode = 'encrypted' }: LoadingViewProps) {
const theme = useTheme();
const [dots, setDots] = useState('');

Expand All @@ -28,6 +30,50 @@ export function LoadingView({ isNewSetup }: LoadingViewProps) {
return () => clearInterval(interval);
}, []);

// Content for "Securing Your Data" stage
if (stage === 'securing') {
return (
<View style={styles.loadingContent}>
<View style={styles.loadingCenter}>
<ActivityIndicator
size="large"
color={theme.colors.primary}
style={{ marginBottom: 24 }}
/>

<Text style={[styles.loadingTitle, { color: theme.colors.foreground }]}>
Securing Your Data{dots}
</Text>

<View
style={[
styles.notice,
{
backgroundColor: theme.colors.card,
borderColor: theme.colors.border,
},
]}
>
<Text style={[styles.noticeText, { color: theme.colors.foreground }]}>
{isNewSetup
? 'We are generating military-grade encryption with 250,000 security iterations to protect your notes. Please wait and do not close the app.'
: 'Verifying your master password and loading encryption keys. This may take a moment.'}
</Text>
<Text
style={[
styles.noticeText,
{ color: theme.colors.foreground, marginTop: 12, fontWeight: '600' },
]}
>
This process can take 2-5 minutes. The app may appear frozen but it&apos;s working{dots}
</Text>
</View>
</View>
</View>
);
}

// Content for "Caching Your Data" stage
return (
<View style={styles.loadingContent}>
<View style={styles.loadingCenter}>
Expand All @@ -38,7 +84,7 @@ export function LoadingView({ isNewSetup }: LoadingViewProps) {
/>

<Text style={[styles.loadingTitle, { color: theme.colors.foreground }]}>
Securing Your Data{dots}
Caching Your Data{dots}
</Text>

<View
Expand All @@ -51,17 +97,19 @@ export function LoadingView({ isNewSetup }: LoadingViewProps) {
]}
>
<Text style={[styles.noticeText, { color: theme.colors.foreground }]}>
{isNewSetup
? 'We are generating military-grade encryption with 250,000 security iterations to protect your notes. Please wait and do not close the app.'
: 'Verifying your master password and loading encryption keys. This may take a moment.'}
{cacheMode === 'decrypted'
? 'Downloading and decrypting all your notes for instant offline access. This improves performance but stores decrypted content locally.'
: 'Downloading all your notes in encrypted form for offline access. Notes will be decrypted on-demand for better security.'}
</Text>
<Text
style={[
styles.noticeText,
{ color: theme.colors.foreground, marginTop: 12, fontWeight: '600' },
]}
>
This process can take 2-5 minutes. The app may appear frozen but it&apos;s working{dots}
{cacheMode === 'decrypted'
? `This may take 5-10 seconds${dots}`
: `This should only take a few seconds${dots}`}
</Text>
</View>
</View>
Expand Down
10 changes: 9 additions & 1 deletion apps/mobile/v1/src/components/MasterPasswordDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useKeyboardHandler } from './useKeyboardHandler';
interface MasterPasswordScreenProps {
userId: string;
isNewSetup: boolean;
loadingStage?: 'securing' | 'caching';
cacheMode?: 'encrypted' | 'decrypted';
onSuccess: (password: string) => Promise<void>;
}

Expand All @@ -21,6 +23,8 @@ interface MasterPasswordScreenProps {
*/
export function MasterPasswordScreen({
isNewSetup,
loadingStage = 'securing',
cacheMode = 'encrypted',
onSuccess,
}: MasterPasswordScreenProps) {
const theme = useTheme();
Expand Down Expand Up @@ -72,7 +76,11 @@ export function MasterPasswordScreen({
onSubmit={handleFormSubmit}
/>
) : (
<LoadingView isNewSetup={isNewSetup} />
<LoadingView
isNewSetup={isNewSetup}
stage={loadingStage}
cacheMode={cacheMode}
/>
)}
</ScrollView>
</Animated.View>
Expand Down
Loading