feat(deployment): prepare app for iOS TestFlight submission

- Update outdated Expo packages to latest compatible versions
- Remove unmaintained expo-random package
- Remove unnecessary @types/react-native package
- Configure eas.json with preview and production profiles for iOS
- Fix updates URL in app.json with correct project ID
- Add /android and /ios to .gitignore to resolve workflow conflict
- Create comprehensive iOS TestFlight submission guide
- Add production flag in theme constants
- Hide development-only Programs tab in production builds
- Remove debug UI and console logs from social feed in production
- Update CHANGELOG.md with TestFlight preparation changes
All checks from expo-doctor now pass (15/15).
This commit is contained in:
DocNR 2025-03-28 21:22:20 -07:00
parent 3f2ababe4f
commit 89504f48e8
12 changed files with 382 additions and 691 deletions

7
.gitignore vendored
View File

@ -16,6 +16,11 @@ web-build/
*.key *.key
*.mobileprovision *.mobileprovision
# Prebuild folders - addressing workflow conflict
# For CNG/Prebuild with EAS Build
/android
/ios
# Metro # Metro
.metro-health-check* .metro-health-check*
@ -38,4 +43,4 @@ yarn-error.*
# The following patterns were generated by expo-cli # The following patterns were generated by expo-cli
expo-env.d.ts expo-env.d.ts
# @end expo-cli # @end expo-cli

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## [Unreleased]
### Added
- TestFlight preparation: Added production flag in theme constants
- TestFlight preparation: Hid development-only Programs tab in production builds
- TestFlight preparation: Removed debug UI and console logs from social feed in production builds
All notable changes to the POWR project will be documented in this file. All notable changes to the POWR project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
@ -14,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Implemented useProfileStats hook with auto-refresh capabilities - Implemented useProfileStats hook with auto-refresh capabilities
- Added proper loading states and error handling - Added proper loading states and error handling
- Created documentation in the new documentation structure - Created documentation in the new documentation structure
- iOS TestFlight build configuration
- Created comprehensive TestFlight submission documentation
- Added production and preview build profiles to eas.json
- Added TestFlight submission configuration
- Created deployment documentation in docs/deployment/ios_testflight_guide.md
## Improved ## Improved
- Enhanced Profile UI - Enhanced Profile UI
@ -22,6 +33,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added inline copy and QR buttons for better usability - Added inline copy and QR buttons for better usability
- Enhanced visual consistency across profile elements - Enhanced visual consistency across profile elements
- Replaced hardcoded follower counts with real-time data - Replaced hardcoded follower counts with real-time data
- Updated project configuration for TestFlight
- Updated outdated packages (expo, expo-dev-client, expo-file-system, expo-router, expo-sqlite, jest-expo)
- Removed unmaintained packages (expo-random)
- Removed unnecessary packages (@types/react-native)
- Fixed updates URL in app.json to use the correct project ID
- Documented workflow conflict between managed and bare configurations
## Fixed
- Prebuild/managed workflow conflict documentation
- Added detailed explanation of the configuration issue
- Documented future decision points for project architecture
- Provided options for resolving the configuration conflict
# Changelog - March 26, 2025 # Changelog - March 26, 2025

View File

@ -67,7 +67,7 @@
"policy": "sdkVersion" "policy": "sdkVersion"
}, },
"updates": { "updates": {
"url": "https://u.expo.dev/your-project-id" "url": "https://u.expo.dev/f3895f49-d9c9-4653-b73b-356f727debe2"
}, },
"extra": { "extra": {
"router": { "router": {
@ -79,4 +79,4 @@
}, },
"owner": "promotus" "owner": "promotus"
} }
} }

View File

@ -8,6 +8,7 @@ import Header from '@/components/Header';
import { useTheme } from '@react-navigation/native'; import { useTheme } from '@react-navigation/native';
import type { CustomTheme } from '@/lib/theme'; import type { CustomTheme } from '@/lib/theme';
import { TabScreen } from '@/components/layout/TabScreen'; import { TabScreen } from '@/components/layout/TabScreen';
import { IS_PRODUCTION } from '@/lib/theme/constants';
const Tab = createMaterialTopTabNavigator(); const Tab = createMaterialTopTabNavigator();
@ -52,12 +53,15 @@ export default function LibraryLayout() {
component={TemplatesScreen} component={TemplatesScreen}
options={{ title: 'Templates' }} options={{ title: 'Templates' }}
/> />
<Tab.Screen {/* Only show Programs tab in development builds */}
name="programs" {!IS_PRODUCTION && (
component={ProgramsScreen} <Tab.Screen
options={{ title: 'Programs' }} name="programs"
/> component={ProgramsScreen}
options={{ title: 'Programs' }}
/>
)}
</Tab.Navigator> </Tab.Navigator>
</TabScreen> </TabScreen>
); );
} }

View File

@ -10,6 +10,7 @@ import { useContactList } from '@/lib/hooks/useContactList';
import { ChevronUp, Bug } from 'lucide-react-native'; import { ChevronUp, Bug } from 'lucide-react-native';
import { withOfflineState } from '@/components/social/SocialOfflineState'; import { withOfflineState } from '@/components/social/SocialOfflineState';
import { useSocialFeed } from '@/lib/hooks/useSocialFeed'; import { useSocialFeed } from '@/lib/hooks/useSocialFeed';
import { IS_PRODUCTION } from '@/lib/theme/constants';
function FollowingScreen() { function FollowingScreen() {
const { isAuthenticated, currentUser } = useNDKCurrentUser(); const { isAuthenticated, currentUser } = useNDKCurrentUser();
@ -18,11 +19,13 @@ function FollowingScreen() {
// Get the user's contact list // Get the user's contact list
const { contacts, isLoading: isLoadingContacts } = useContactList(currentUser?.pubkey); const { contacts, isLoading: isLoadingContacts } = useContactList(currentUser?.pubkey);
// Add debug logging for contact list // Add debug logging for contact list (only in development)
React.useEffect(() => { React.useEffect(() => {
console.log(`[FollowingScreen] Contact list has ${contacts.length} contacts`); if (!IS_PRODUCTION) {
if (contacts.length > 0) { console.log(`[FollowingScreen] Contact list has ${contacts.length} contacts`);
console.log(`[FollowingScreen] First few contacts: ${contacts.slice(0, 3).join(', ')}`); if (contacts.length > 0) {
console.log(`[FollowingScreen] First few contacts: ${contacts.slice(0, 3).join(', ')}`);
}
} }
}, [contacts.length]); }, [contacts.length]);
@ -77,7 +80,9 @@ function FollowingScreen() {
// Update loadedContactsCount when contacts change // Update loadedContactsCount when contacts change
React.useEffect(() => { React.useEffect(() => {
if (contacts.length > 0 && contacts.length !== loadedContactsCount) { if (contacts.length > 0 && contacts.length !== loadedContactsCount) {
console.log(`[FollowingScreen] Contact list changed from ${loadedContactsCount} to ${contacts.length} contacts`); if (!IS_PRODUCTION) {
console.log(`[FollowingScreen] Contact list changed from ${loadedContactsCount} to ${contacts.length} contacts`);
}
setLoadedContactsCount(contacts.length); setLoadedContactsCount(contacts.length);
// Reset hasLoadedWithContacts flag when contacts change // Reset hasLoadedWithContacts flag when contacts change
setHasLoadedWithContacts(false); setHasLoadedWithContacts(false);
@ -99,7 +104,9 @@ function FollowingScreen() {
contactRefreshAttempts < maxContactRefreshAttempts; contactRefreshAttempts < maxContactRefreshAttempts;
if (shouldRefresh) { if (shouldRefresh) {
console.log(`[FollowingScreen] Refreshing feed with ${contacts.length} contacts (attempt ${contactRefreshAttempts + 1}/${maxContactRefreshAttempts})`); if (!IS_PRODUCTION) {
console.log(`[FollowingScreen] Refreshing feed with ${contacts.length} contacts (attempt ${contactRefreshAttempts + 1}/${maxContactRefreshAttempts})`);
}
setIsRefreshingWithContacts(true); setIsRefreshingWithContacts(true);
setContactRefreshAttempts(prev => prev + 1); setContactRefreshAttempts(prev => prev + 1);
@ -111,7 +118,9 @@ function FollowingScreen() {
setIsRefreshingWithContacts(false); setIsRefreshingWithContacts(false);
}) })
.catch(error => { .catch(error => {
console.error('[FollowingScreen] Error refreshing feed:', error); if (!IS_PRODUCTION) {
console.error('[FollowingScreen] Error refreshing feed:', error);
}
setIsRefreshingWithContacts(false); setIsRefreshingWithContacts(false);
// Prevent infinite retries by marking as loaded after max attempts // Prevent infinite retries by marking as loaded after max attempts
@ -183,7 +192,9 @@ function FollowingScreen() {
} }
} }
} catch (error) { } catch (error) {
console.error('[FollowingScreen] Error refreshing feed:', error); if (!IS_PRODUCTION) {
console.error('[FollowingScreen] Error refreshing feed:', error);
}
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
@ -193,25 +204,33 @@ function FollowingScreen() {
const checkRelayConnections = useCallback(() => { const checkRelayConnections = useCallback(() => {
if (!ndk) return; if (!ndk) return;
console.log("=== RELAY CONNECTION STATUS ==="); // Only log in development mode
if (ndk.pool && ndk.pool.relays) { if (!IS_PRODUCTION) {
console.log(`Connected to ${ndk.pool.relays.size} relays:`); console.log("=== RELAY CONNECTION STATUS ===");
ndk.pool.relays.forEach((relay) => { if (ndk.pool && ndk.pool.relays) {
console.log(`- ${relay.url}: ${relay.status}`); console.log(`Connected to ${ndk.pool.relays.size} relays:`);
}); ndk.pool.relays.forEach((relay) => {
} else { console.log(`- ${relay.url}: ${relay.status}`);
console.log("No relay pool or connections available"); });
} else {
console.log("No relay pool or connections available");
}
console.log("===============================");
} }
console.log("===============================");
}, [ndk]); }, [ndk]);
// Handle post selection - simplified for testing // Handle post selection - simplified for testing
const handlePostPress = useCallback((entry: any) => { const handlePostPress = useCallback((entry: any) => {
// Just show an alert with the entry info for testing // Just show an alert with the entry info for testing
alert(`Selected ${entry.type} with ID: ${entry.id || entry.eventId}`); if (!IS_PRODUCTION) {
alert(`Selected ${entry.type} with ID: ${entry.id || entry.eventId}`);
// Alternatively, log to console for debugging
console.log(`Selected ${entry.type}:`, entry);
}
// Alternatively, log to console for debugging // In production, this would navigate to the post detail screen
console.log(`Selected ${entry.type}:`, entry); // TODO: Implement proper post detail navigation for production
}, []); }, []);
// Memoize render item function // Memoize render item function
@ -267,15 +286,17 @@ function FollowingScreen() {
: "No content from followed users found. Try following more users or check your relay connections."} : "No content from followed users found. Try following more users or check your relay connections."}
</Text> </Text>
{/* Debug toggle */} {/* Debug toggle - only shown in development */}
<TouchableOpacity {!IS_PRODUCTION && (
className="mt-4 bg-gray-200 py-2 px-4 rounded" <TouchableOpacity
onPress={() => setShowDebug(!showDebug)} className="mt-4 bg-gray-200 py-2 px-4 rounded"
> onPress={() => setShowDebug(!showDebug)}
<Text>{showDebug ? "Hide" : "Show"} Debug Info</Text> >
</TouchableOpacity> <Text>{showDebug ? "Hide" : "Show"} Debug Info</Text>
</TouchableOpacity>
)}
{showDebug && ( {!IS_PRODUCTION && showDebug && (
<View className="mt-4 p-4 bg-gray-100 rounded w-full"> <View className="mt-4 p-4 bg-gray-100 rounded w-full">
<Text className="text-xs">User pubkey: {currentUser?.pubkey?.substring(0, 12)}...</Text> <Text className="text-xs">User pubkey: {currentUser?.pubkey?.substring(0, 12)}...</Text>
<Text className="text-xs">Authenticated: {isAuthenticated ? "Yes" : "No"}</Text> <Text className="text-xs">Authenticated: {isAuthenticated ? "Yes" : "No"}</Text>
@ -305,16 +326,18 @@ function FollowingScreen() {
return ( return (
<View className="flex-1"> <View className="flex-1">
{/* Debug toggle button */} {/* Debug toggle button - only shown in development */}
<TouchableOpacity {!IS_PRODUCTION && (
className="absolute top-2 right-2 z-10 bg-gray-200 p-2 rounded-full" <TouchableOpacity
onPress={() => setShowDebug(!showDebug)} className="absolute top-2 right-2 z-10 bg-gray-200 p-2 rounded-full"
> onPress={() => setShowDebug(!showDebug)}
<Bug size={16} color="#666" /> >
</TouchableOpacity> <Bug size={16} color="#666" />
</TouchableOpacity>
)}
{/* Debug panel */} {/* Debug panel - only shown in development */}
{showDebug && <DebugControls />} {!IS_PRODUCTION && showDebug && <DebugControls />}
{showNewButton && ( {showNewButton && (
<TouchableOpacity <TouchableOpacity

BIN
assets/splash.mp4 Normal file

Binary file not shown.

BIN
assets/v1-splash.mp4 Normal file

Binary file not shown.

View File

@ -0,0 +1,96 @@
# iOS TestFlight Submission Guide
This guide documents the process for creating and submitting POWR to TestFlight for iOS testing.
## Project Configuration Issues
### Prebuild/Managed Workflow Conflict
The project currently has a "mixed" state that needs to be addressed:
- Native iOS and Android folders exist (bare workflow)
- Configuration exists in app.json that would normally be used in a managed workflow
When both native folders and app.json configs exist, EAS Build will use the native project settings and ignore certain app.json configurations including:
- orientation
- icon
- scheme
- userInterfaceStyle
- splash
- ios/android configuration
- plugins
- androidStatusBar
**TODO: After TestFlight validation, decide on one of these approaches:**
1. Commit to bare workflow: Keep native folders and move all configuration to them
2. Commit to managed workflow: Remove native folders and let Expo handle native code generation
## Fixed Issues
The following issues were addressed to prepare for TestFlight:
1. Updated outdated packages:
- expo: ~52.0.41 (was 52.0.35)
- expo-dev-client: ~5.0.15 (was 5.0.12)
- expo-file-system: ~18.0.12 (was 18.0.10)
- expo-router: ~4.0.19 (was 4.0.17)
- expo-sqlite: ~15.1.3 (was 15.1.2)
- jest-expo: ~52.0.6 (was 52.0.4)
2. Removed unmaintained and unnecessary packages:
- expo-random: Removed as it's flagged as unmaintained
- @types/react-native: Removed as types are included with react-native
3. Added proper iOS build configurations in eas.json:
- Added preview build profile for internal testing
- Added production build profile for App Store submission
4. Fixed updates URL in app.json to use the correct project ID
5. Fixed prebuild/managed workflow conflict:
- Added /android and /ios folders to .gitignore as recommended by expo-doctor
- This approach tells Git to ignore native folders while still allowing EAS Build to use them
- Addresses the warning about app.json configuration fields not being synced in non-CNG projects
## TestFlight Build Process
To create and submit a build to TestFlight:
1. Update Apple credentials in eas.json:
```json
"submit": {
"production": {
"ios": {
"appleId": "YOUR_APPLE_ID_EMAIL",
"ascAppId": "YOUR_APP_STORE_CONNECT_APP_ID",
"appleTeamId": "YOUR_APPLE_TEAM_ID"
}
}
}
```
2. Create a build for internal testing (preview):
```
eas build --platform ios --profile preview
```
3. Create a production build for TestFlight:
```
eas build --platform ios --profile production
```
4. Submit the build to TestFlight:
```
eas submit --platform ios --latest
```
## Troubleshooting
- If you encounter issues with the mixed configuration state, consider fully committing to either the bare or managed workflow
- For build errors related to native code, check the iOS logs in the EAS build output
- For App Store Connect submission errors, verify your app metadata and screenshots in App Store Connect
## References
- [Expo Application Services Documentation](https://docs.expo.dev/eas/)
- [Expo Prebuild Documentation](https://docs.expo.dev/workflow/prebuild/)
- [TestFlight Documentation](https://developer.apple.com/testflight/)

View File

@ -1,21 +1,42 @@
{ {
"cli": { "cli": {
"version": ">= 3.7.2" "version": ">= 3.7.2"
},
"build": {
"development-simulator": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
}, },
"build": { "development": {
"development-simulator": { "developmentClient": true,
"developmentClient": true, "distribution": "internal",
"distribution": "internal", "android": {
"ios": { "buildType": "apk"
"simulator": true }
} },
}, "preview": {
"development": { "distribution": "internal",
"developmentClient": true, "ios": {
"distribution": "internal", "resourceClass": "m1-medium"
"android": { }
"buildType": "apk" },
} "production": {
"autoIncrement": true,
"ios": {
"resourceClass": "m1-medium"
} }
} }
} },
"submit": {
"production": {
"ios": {
"appleId": "YOUR_APPLE_ID_EMAIL",
"ascAppId": "YOUR_APP_STORE_CONNECT_APP_ID",
"appleTeamId": "YOUR_APPLE_TEAM_ID"
}
}
}
}

View File

@ -1,6 +1,24 @@
// lib/theme/constants.ts // lib/theme/constants.ts
import { COLORS } from './colors'; import { COLORS } from './colors';
/**
* Application configuration
*/
/**
* Set to true for production builds (TestFlight, App Store)
* This should be automatically configured based on the EAS build profile
*
* For local development, keep this as false
* For TestFlight/App Store builds, set to true
*/
export const IS_PRODUCTION = true;
/**
* App version information
*/
export const APP_VERSION = '1.0.0';
export interface NavigationThemeColors { export interface NavigationThemeColors {
background: string; background: string;
border: string; border: string;
@ -39,4 +57,4 @@ export const NAV_THEME: {
tabInactive: COLORS.dark.mutedForeground, tabInactive: COLORS.dark.mutedForeground,
tabIndicator: COLORS.purple.DEFAULT, tabIndicator: COLORS.purple.DEFAULT,
}, },
}; };

741
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -63,23 +63,22 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"expo": "^52.0.35", "expo": "~52.0.41",
"expo-av": "~15.0.2", "expo-av": "~15.0.2",
"expo-crypto": "~14.0.2", "expo-crypto": "~14.0.2",
"expo-dev-client": "~5.0.12", "expo-dev-client": "~5.0.15",
"expo-file-system": "~18.0.10", "expo-file-system": "~18.0.12",
"expo-linking": "~7.0.4", "expo-linking": "~7.0.4",
"expo-navigation-bar": "~4.0.8", "expo-navigation-bar": "~4.0.8",
"expo-nip55": "^0.1.5", "expo-nip55": "^0.1.5",
"expo-random": "^14.0.1", "expo-router": "~4.0.19",
"expo-router": "~4.0.16",
"expo-secure-store": "~14.0.1", "expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.22", "expo-splash-screen": "~0.29.22",
"expo-sqlite": "~15.1.2", "expo-sqlite": "~15.1.3",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8", "expo-system-ui": "~4.0.8",
"jest": "~29.7.0", "jest": "~29.7.0",
"jest-expo": "~52.0.3", "jest-expo": "~52.0.6",
"lucide-react-native": "^0.378.0", "lucide-react-native": "^0.378.0",
"nativewind": "^4.1.23", "nativewind": "^4.1.23",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
@ -106,7 +105,6 @@
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/lodash": "^4.17.15", "@types/lodash": "^4.17.15",
"@types/react": "~18.3.12", "@types/react": "~18.3.12",
"@types/react-native": "^0.72.8",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"babel-plugin-module-resolver": "^5.0.2", "babel-plugin-module-resolver": "^5.0.2",
"expo-haptics": "^14.0.1", "expo-haptics": "^14.0.1",