feat: Add profile screen and various improvements#10
Conversation
- Add new Profile screen with user information display - Add Google Sign Out and Revoke Access functionality - Add permissions layout for better organization - Add TypeScript types for react-native-console-view - Add misc utilities for time formatting - Add Gradle wrapper files for Android builds - Update tab layout to include Profile tab - Remove deprecated cellular and location components - Improve debug screen and login functionality - Update various permission demos - Update dependencies in package.json
There was a problem hiding this comment.
Pull request overview
This PR adds a new Profile screen and various improvements to the React Native app, including enhanced permission demos, better navigation patterns, and utility functions for time formatting.
Changes:
- Added a Profile screen with Google authentication management (sign out/revoke access)
- Significantly expanded permission demos for Telephony/SMS, Camera/Microphone, and Location
- Added time formatting utilities (
timeAgofunction) and integrated into LogViewer - Refactored login flow to auto-redirect to profile when authenticated
- Reorganized tab navigation, replacing "Cellular" tab with "Profile" tab
- Fixed camera toggle bug and added better error handling for camera types
Reviewed changes
Copilot reviewed 23 out of 26 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
utils/misc.utils.ts |
New time formatting utilities with timeAgo and parseTimeString functions |
utils/camera.utils.ts |
Fixed toggleCameraType bug and improved error handling for camera types |
types/react-native-console-view.d.ts |
Added TypeScript type definitions for react-native-console-view |
components/permissions/TelephonySmsPermissionDemo.tsx |
Major expansion with cellular info, contacts pagination, call logs, and SMS functionality |
components/permissions/LocationPermissionDemo.tsx |
Integrated map display and location hook for better UX |
components/permissions/CameraMicrophonePermissionDemo.tsx |
Expanded with live camera preview, controls, and capture functionality |
components/permissions/NotificationsPermissionDemo.tsx |
Added trigger sample notification button |
components/LogViewer.tsx |
Integrated timeAgo utility with auto-refresh every 30 seconds |
components/CustomHeader.tsx |
Updated to use safe area insets for proper padding on notched devices |
app/(tabs)/profile.tsx |
New profile screen with user information and authentication controls |
app/login.tsx |
Refactored to auto-redirect authenticated users to profile |
app/debug.tsx |
Enhanced with exception handler controls and crash test buttons |
app/_layout.tsx |
Cleaned up by removing inline exception handlers |
app/modal.tsx |
Updated to redirect to camera permissions instead of embedding camera |
app/(tabs)/index.tsx |
Simplified home screen by removing location/map display |
app/(tabs)/_layout.tsx |
Updated tab layout with Profile tab and improved permissions tab navigation |
app/(tabs)/permissions/index.tsx |
Enhanced UI with icons, colors, and better navigation |
app/(tabs)/permissions/_layout.tsx |
New layout file for permissions stack navigation |
package.json |
Added expo-contacts and expo-sms dependencies |
build/android/* |
Added Gradle wrapper files for Android builds |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| tabPress: (e: { preventDefault: () => void }) => { | ||
| // If already on a permissions sub-page (not the index), navigate to permissions index | ||
| const isOnPermissionsSubPage = pathname.startsWith('/permissions/') && pathname !== '/permissions'; | ||
| const isOnPermissionsIndex = pathname === '/permissions' || pathname === '/(tabs)/permissions'; | ||
|
|
||
| if (isOnPermissionsSubPage || isOnPermissionsIndex) { | ||
| // Prevent default tab behavior | ||
| e.preventDefault(); | ||
| // Navigate to permissions index, replacing the current route to reset the stack | ||
| router.replace('/(tabs)/permissions'); | ||
| } |
There was a problem hiding this comment.
The tab press logic on lines 59-62 has redundant conditions. When isOnPermissionsIndex is true, the code prevents default and calls router.replace('/(tabs)/permissions'), which navigates to the same page the user is already on. This creates unnecessary navigation and may cause flickering. The condition should only prevent default and navigate when on a sub-page, not when already on the index. Remove isOnPermissionsIndex from the if condition on line 62.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| export const timeAgo = (timestamp: Date | string | number): string => { | ||
| const now = new Date(); | ||
| const date = timestamp instanceof Date ? timestamp : new Date(timestamp); | ||
|
|
||
| const diffMs = now.getTime() - date.getTime(); | ||
| const diffSecs = Math.floor(diffMs / 1000); | ||
| const diffMins = Math.floor(diffSecs / 60); | ||
| const diffHours = Math.floor(diffMins / 60); | ||
| const diffDays = Math.floor(diffHours / 24); | ||
| const diffWeeks = Math.floor(diffDays / 7); | ||
| const diffMonths = Math.floor(diffDays / 30); | ||
| const diffYears = Math.floor(diffDays / 365); | ||
|
|
||
| if (diffSecs < 60) { | ||
| return '<1 min ago'; | ||
| } else if (diffMins === 1) { | ||
| return '1 min ago'; | ||
| } else if (diffMins < 60) { | ||
| return `${diffMins} mins ago`; | ||
| } else if (diffHours === 1) { | ||
| return '1 hour ago'; | ||
| } else if (diffHours < 24) { | ||
| return `${diffHours} hours ago`; | ||
| } else if (diffDays === 1) { | ||
| return '1 day ago'; | ||
| } else if (diffDays < 7) { | ||
| return `${diffDays} days ago`; | ||
| } else if (diffWeeks === 1) { | ||
| return '1 week ago'; | ||
| } else if (diffWeeks < 4) { | ||
| return `${diffWeeks} weeks ago`; | ||
| } else if (diffMonths === 1) { | ||
| return '1 month ago'; | ||
| } else if (diffMonths < 12) { | ||
| return `${diffMonths} months ago`; | ||
| } else if (diffYears === 1) { | ||
| return '1 year ago'; | ||
| } else { | ||
| return `${diffYears} years ago`; | ||
| } | ||
| }; |
There was a problem hiding this comment.
The timeAgo function doesn't handle future dates. If the timestamp is in the future (e.g., system clock issues or user-provided date), diffMs will be negative, causing incorrect "time ago" strings like "-5 mins ago". Consider adding a check at the beginning to handle future dates, either by returning "just now" or "in the future".
| const diffMonths = Math.floor(diffDays / 30); | ||
| const diffYears = Math.floor(diffDays / 365); |
There was a problem hiding this comment.
The month calculation (diffDays / 30) and year calculation (diffDays / 365) use simplified assumptions that don't account for varying month lengths or leap years. For example, "1 month ago" could represent anywhere from 28-31 days. While this is acceptable for approximate "time ago" displays, consider documenting this limitation or using a more accurate date library like date-fns if precise calculations are needed.
| if (loadMore && contactsCursor) { | ||
| options.pageOffset = contactsCursor; | ||
| } | ||
|
|
||
| const { data, hasNextPage, hasPreviousPage } = await Contacts.getContactsAsync(options); | ||
|
|
||
| if (loadMore) { | ||
| setContacts(prev => [...prev, ...data]); | ||
| } else { | ||
| setContacts(data); | ||
| } | ||
|
|
||
| setHasMoreContacts(hasNextPage); | ||
| if (data.length > 0) { | ||
| setContactsCursor(data[data.length - 1].id); |
There was a problem hiding this comment.
The pagination implementation uses options.pageOffset = contactsCursor where contactsCursor is a contact ID (line 195). However, the expo-contacts API typically expects pageOffset to be a numeric index, not an ID. This may cause pagination to fail. Review the expo-contacts documentation for the correct pagination approach - you may need to use a numeric offset or a different pagination strategy.
| // Exception handler callbacks | ||
| const jsExceptionHandler = (error: any, isFatal: boolean) => { | ||
| console.log('ExceptionHandler called with error: ', error, 'isFatal: ', isFatal); | ||
| }; | ||
|
|
||
| const nativeExceptionHandler = (exceptionString: string) => { | ||
| console.log('Native ExceptionHandler called with exception: ', exceptionString); | ||
| }; | ||
|
|
||
| const handleSetJSException = () => { | ||
| setJSExceptionHandler(jsExceptionHandler, true); | ||
| console.log('JS Exception Handler set!'); | ||
| }; | ||
|
|
||
| const handleGetJSException = () => { | ||
| const currentHandler = getJSExceptionHandler(); | ||
| console.log('Current JS Exception Handler: ', currentHandler.toString()); | ||
| }; | ||
|
|
||
| const handleSetNativeException = () => { | ||
| setNativeExceptionHandler(nativeExceptionHandler, false, false); | ||
| console.log('Native Exception Handler set!'); | ||
| }; |
There was a problem hiding this comment.
The exception handlers (jsExceptionHandler and nativeExceptionHandler) are defined as module-level constants outside the component. While this works, calling setJSExceptionHandler or setNativeExceptionHandler multiple times will overwrite the global handlers each time. This is fine for a debug screen, but consider adding user feedback or checks to prevent redundant handler registration, or indicate to users that handlers are already set.
|
@digitalnomad91 I've opened a new pull request, #11, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Summary
This PR adds a new Profile screen and various improvements to the app:
New Features
Improvements
Removed
Dependencies
Testing
Note
This commit excludes: