Files
crowd-funder-for-time-pwa/scripts/build-ios.sh
Jose Olarte III ae49c0e907 feat(ios): implement native share target for images
Implement iOS Share Extension to enable native image sharing from Photos
and other apps directly into TimeSafari. Users can now share images from
the iOS share sheet, which will open in SharedPhotoView for use as gifts
or profile pictures.

iOS Native Implementation:
- Add TimeSafariShareExtension target with ShareViewController
- Configure App Groups for data sharing between extension and main app
- Implement ShareViewController to process shared images and convert to base64
- Store shared image data in App Group UserDefaults
- Add ShareImageBridge utility to read shared data from App Group
- Update AppDelegate to handle shared-photo deep link and bridge data to JS

JavaScript Integration:
- Add checkAndStoreNativeSharedImage() in main.capacitor.ts to read shared
  images from native layer via temporary file bridge
- Convert base64 data to data URL format for compatibility with base64ToBlob
- Integrate with existing SharedPhotoView component
- Add "shared-photo" to deep link validation schema

Build System:
- Integrate Xcode 26 / CocoaPods compatibility workaround into build-ios.sh
- Add run_pod_install_with_workaround() for explicit pod install
- Add run_cap_sync_with_workaround() for Capacitor sync (which runs pod
  install internally)
- Automatically detect project format version 70 and apply workaround
- Remove standalone pod-install-workaround.sh script

Code Cleanup:
- Remove verbose debug logs from ShareViewController, AppDelegate, and
  main.capacitor.ts
- Retain essential logger calls for production debugging

Documentation:
- Add ios-share-extension-setup.md with manual Xcode setup instructions
- Add ios-share-extension-git-commit-guide.md for version control best practices
- Add ios-share-implementation-status.md tracking implementation progress
- Add native-share-target-implementation.md with overall architecture
- Add xcode-26-cocoapods-workaround.md documenting the compatibility issue

The implementation uses a temporary file bridge (AppDelegate writes to Documents
directory, JS reads via Capacitor Filesystem plugin) as a workaround for
Capacitor plugin auto-discovery issues. This can be improved in the future by
properly registering ShareImagePlugin in Capacitor's plugin registry.
2025-11-24 20:46:58 +08:00

606 lines
20 KiB
Bash
Executable File

#!/bin/bash
# build-ios.sh
# Author: Matthew Raymer
# Description: iOS build script for TimeSafari application
# Date: 2025-07-11
# Exit on any error
set -e
# Source common utilities
source "$(dirname "$0")/common.sh"
# Default values
BUILD_MODE="development"
BUILD_TYPE="debug"
OPEN_STUDIO=false
BUILD_IPA=false
BUILD_APP=false
CLEAN_ONLY=false
SYNC_ONLY=false
ASSETS_ONLY=false
DEPLOY_APP=false
AUTO_RUN=false
CUSTOM_API_IP=""
# Function to print iOS-specific usage
print_ios_usage() {
echo "Usage: $0 [options]"
echo ""
echo "iOS Build Options:"
echo " --dev, --development Build for development environment"
echo " --test Build for testing environment"
echo " --prod, --production Build for production environment"
echo " --debug Build debug app (default)"
echo " --release Build release app"
echo " --studio Open Xcode after build"
echo " --ipa Build IPA file"
echo " --app Build app bundle"
echo " --clean Clean build artifacts only"
echo " --sync Sync Capacitor only"
echo " --assets Generate assets only"
echo " --deploy Deploy app to connected device"
echo " --auto-run Auto-run app after build"
echo " --api-ip <ip> Custom IP address for claim API (uses Capacitor default)"
echo ""
echo "Common Options:"
echo " -h, --help Show this help message"
echo " -v, --verbose Enable verbose logging"
echo ""
echo "Examples:"
echo " $0 --dev --studio # Development build + open Xcode"
echo " $0 --prod --ipa # Production IPA build"
echo " $0 --test --app # Testing app build"
echo " $0 --test --auto-run # Test build + auto-run"
echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device"
echo " $0 --dev # Dev build with Capacitor default"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo ""
}
# Function to parse iOS-specific arguments
parse_ios_args() {
local args=("$@")
local i=0
while [ $i -lt ${#args[@]} ]; do
local arg="${args[$i]}"
case $arg in
--dev|--development)
BUILD_MODE="development"
;;
--test)
BUILD_MODE="test"
;;
--prod|--production)
BUILD_MODE="production"
;;
--debug)
BUILD_TYPE="debug"
;;
--release)
BUILD_TYPE="release"
;;
--studio)
OPEN_STUDIO=true
;;
--ipa)
BUILD_IPA=true
;;
--app)
BUILD_APP=true
;;
--clean)
CLEAN_ONLY=true
;;
--sync)
SYNC_ONLY=true
;;
--assets)
ASSETS_ONLY=true
;;
--deploy)
DEPLOY_APP=true
;;
--auto-run)
AUTO_RUN=true
;;
--api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}"
i=$((i + 1)) # Skip the next argument
else
log_error "Error: --api-ip requires an IP address"
exit 1
fi
;;
--api-ip=*)
CUSTOM_API_IP="${arg#*=}"
;;
-h|--help)
print_ios_usage
exit 0
;;
-v|--verbose)
set -x
;;
*)
log_warn "Unknown argument: $arg"
;;
esac
i=$((i + 1))
done
}
# Function to validate iOS environment
validate_ios_environment() {
log_info "Validating iOS build environment..."
# Check for Xcode
if ! command -v xcodebuild &> /dev/null; then
log_error "Xcode not found. Please install Xcode and command line tools."
exit 1
fi
# Check for iOS Simulator
if ! command -v xcrun &> /dev/null; then
log_error "Xcode command line tools not found. Please install with: xcode-select --install"
exit 1
fi
# Check for Capacitor
if ! command -v npx &> /dev/null; then
log_error "npx not found. Please install Node.js and npm."
exit 1
fi
# Check for iOS platform
if [ ! -d "ios" ]; then
log_error "iOS platform not found. Please run: npx cap add ios"
exit 1
fi
log_success "iOS build environment validated"
}
# Function to check iOS resources
check_ios_resources() {
log_info "Checking iOS resources..."
# Check for required assets
if [ ! -f "assets/icon.png" ]; then
log_warn "App icon not found at assets/icon.png"
fi
if [ ! -f "assets/splash.png" ]; then
log_warn "Splash screen not found at assets/splash.png"
fi
# Check for iOS-specific files
if [ ! -f "ios/App/App/Info.plist" ]; then
log_warn "Info.plist not found"
fi
if [ ! -f "ios/App/App/AppDelegate.swift" ]; then
log_warn "AppDelegate.swift not found"
fi
log_success "iOS resource check completed"
}
# Function to clean iOS build
clean_ios_build() {
log_info "Cleaning iOS build artifacts..."
# Clean Xcode build (temporary output directory)
if [ -d "ios/App/build" ]; then
rm -rf ios/App/build/
log_debug "Cleaned ios/App/build/"
fi
# Clean DerivedData
if [ -d "ios/App/DerivedData" ]; then
rm -rf ios/App/DerivedData/
log_debug "Cleaned ios/App/DerivedData/"
fi
# Clean Capacitor (using npm script instead of invalid cap clean command)
npm run clean:ios || true
log_success "iOS build cleaned"
}
# Function to build iOS app
build_ios_app() {
local build_config=""
local scheme="App"
local destination=""
if [ "$BUILD_TYPE" = "debug" ]; then
build_config="Debug"
destination="platform=iOS Simulator,name=iPhone 15 Pro"
else
build_config="Release"
destination="platform=iOS,id=auto"
fi
log_info "Building iOS app (${build_config})..."
cd ios/App
# Build the app
xcodebuild -workspace App.xcworkspace \
-scheme "$scheme" \
-configuration "$build_config" \
-destination "$destination" \
build \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO
cd ../..
log_success "iOS app built successfully"
}
# Function to deploy to device
deploy_ios_app() {
log_info "Deploy-app mode: building app and deploying to device"
# Check for connected device
local devices=$(xcrun devicectl list devices --json | grep -c '"state":"booted"' || echo "0")
if [ "$devices" -eq 0 ]; then
log_error "No iOS device connected. Please connect a device and try again."
exit 1
fi
# Build app for device
BUILD_TYPE="debug"
build_ios_app
# Get device ID
local device_id=$(xcrun devicectl list devices --json | grep -o '"identifier":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$device_id" ]; then
log_error "Could not find device ID. Please ensure device is connected and unlocked."
exit 1
fi
# Install app on device
log_info "Installing app on device..."
xcrun devicectl device install app --device "$device_id" ios/App/build/Debug-iphoneos/App.app
log_success "iOS app deployed successfully to device!"
log_info "You can now run the app with: npx cap run ios"
}
# Function to auto-run iOS app
auto_run_ios_app() {
log_step "Auto-running iOS app..."
# Check if we're in debug mode (simulator) or release mode (device)
if [ "$BUILD_TYPE" = "debug" ]; then
log_info "Launching iOS Simulator and installing app"
safe_execute "Launching app" "npx cap run ios" || {
log_error "Failed to launch iOS app on simulator"
return 1
}
else
log_info "Building and installing on real device"
# For release builds, we need to use a different approach
# since npx cap run ios is primarily for simulators
log_warn "Auto-run for release builds requires manual device setup"
log_info "Please use Xcode to run the app on your device"
return 1
fi
log_success "iOS app launched successfully!"
}
# Parse command line arguments
parse_ios_args "$@"
# Print build header
print_header "TimeSafari iOS Build Process"
log_info "Starting iOS build process at $(date)"
log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE"
# Setup environment for Capacitor build
setup_build_env "capacitor" "$BUILD_MODE"
# Override API servers for iOS development when custom IP is specified
if [ "$BUILD_MODE" = "development" ] && [ -n "$CUSTOM_API_IP" ]; then
# Use custom IP for physical device development
export VITE_DEFAULT_ENDORSER_API_SERVER="http://${CUSTOM_API_IP}:3000"
export VITE_DEFAULT_PARTNER_API_SERVER="http://${CUSTOM_API_IP}:3000"
log_info "iOS development mode: Using custom IP ${CUSTOM_API_IP} for physical device"
fi
# Setup application directories
setup_app_directories
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env"
fi
# Validate iOS environment
validate_ios_environment
# Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then
log_info "Clean-only mode: cleaning build artifacts"
clean_ios_build
safe_execute "Cleaning dist directory" "clean_build_artifacts dist" || exit 1
log_success "Clean completed successfully!"
exit 0
fi
# Handle sync-only mode
if [ "$SYNC_ONLY" = true ]; then
log_info "Sync-only mode: syncing with Capacitor"
safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
log_success "Sync completed successfully!"
exit 0
fi
# Handle assets-only mode
if [ "$ASSETS_ONLY" = true ]; then
log_info "Assets-only mode: generating assets"
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
log_success "Assets generation completed successfully!"
exit 0
fi
# Handle deploy-app mode
if [ "$DEPLOY_APP" = true ]; then
deploy_ios_app
exit 0
fi
# Step 1: Check iOS resources
check_ios_resources
# Step 2: Clean iOS build
safe_execute "Cleaning iOS build" "clean_ios_build" || exit 1
# Step 3: Clean dist directory
log_info "Cleaning dist directory..."
clean_build_artifacts "dist"
# Step 4: Run TypeScript type checking for test and production builds
if [ "$BUILD_MODE" = "production" ] || [ "$BUILD_MODE" = "test" ]; then
log_info "Running TypeScript type checking for $BUILD_MODE mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $BUILD_MODE mode!"
exit 2
fi
log_success "TypeScript type checking completed for $BUILD_MODE mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
# Step 5: Build Capacitor version with mode
if [ "$BUILD_MODE" = "development" ]; then
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
elif [ "$BUILD_MODE" = "test" ]; then
safe_execute "Building Capacitor version (test)" "npm run build:capacitor -- --mode test" || exit 3
elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi
# Step 6: Install CocoaPods dependencies (with Xcode 26 workaround)
# ===================================================================
# WORKAROUND: Xcode 26 / CocoaPods Compatibility Issue
# ===================================================================
# Xcode 26 uses project format version 70, but CocoaPods' xcodeproj gem
# (1.27.0) only supports up to version 56. This causes pod install to fail.
#
# This workaround temporarily downgrades the project format to 56, runs
# pod install, then restores it to 70. Xcode will automatically upgrade
# it back to 70 when opened, which is fine.
#
# NOTE: Both explicit pod install AND Capacitor sync (which runs pod install
# internally) need this workaround. See run_pod_install_with_workaround()
# and run_cap_sync_with_workaround() functions below.
#
# TO REMOVE THIS WORKAROUND IN THE FUTURE:
# 1. Check if xcodeproj gem has been updated: bundle exec gem list xcodeproj
# 2. Test if pod install works without the workaround
# 3. If it works, remove both workaround functions below
# 4. Replace with:
# - safe_execute "Installing CocoaPods dependencies" "cd ios/App && bundle exec pod install && cd ../.." || exit 6
# - safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
# 5. Update this comment to indicate the workaround has been removed
# ===================================================================
run_pod_install_with_workaround() {
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
log_info "Installing CocoaPods dependencies (with Xcode 26 workaround)..."
# Check if project file exists
if [ ! -f "$PROJECT_FILE" ]; then
log_error "Project file not found: $PROJECT_FILE"
return 1
fi
# Check current format version
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
if [ -z "$current_version" ]; then
log_error "Could not determine project format version"
return 1
fi
log_debug "Current project format version: $current_version"
# Only apply workaround if format is 70
if [ "$current_version" = "70" ]; then
log_debug "Applying Xcode 26 workaround: temporarily downgrading to format 56"
# Downgrade to format 56 (supported by CocoaPods)
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
log_error "Failed to downgrade project format"
return 1
fi
# Run pod install
log_info "Running pod install..."
if ! (cd ios/App && bundle exec pod install && cd ../..); then
log_error "pod install failed"
# Try to restore format even on failure
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
return 1
fi
# Restore to format 70
log_debug "Restoring project format to 70..."
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
fi
log_success "CocoaPods dependencies installed successfully"
else
# Format is not 70, run pod install normally
log_debug "Project format is $current_version, running pod install normally"
if ! (cd ios/App && bundle exec pod install && cd ../..); then
log_error "pod install failed"
return 1
fi
log_success "CocoaPods dependencies installed successfully"
fi
}
safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaround" || exit 6
# Step 6.5: Sync with Capacitor (also needs workaround since it runs pod install internally)
# Capacitor sync internally runs pod install, so we need to apply the workaround here too
run_cap_sync_with_workaround() {
local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
# Check current format version
local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
if [ -z "$current_version" ]; then
log_error "Could not determine project format version for Capacitor sync"
return 1
fi
# Only apply workaround if format is 70
if [ "$current_version" = "70" ]; then
log_debug "Applying Xcode 26 workaround for Capacitor sync: temporarily downgrading to format 56"
# Downgrade to format 56 (supported by CocoaPods)
if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
log_error "Failed to downgrade project format for Capacitor sync"
return 1
fi
# Run Capacitor sync (which will run pod install internally)
log_info "Running Capacitor sync..."
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
# Try to restore format even on failure
sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
return 1
fi
# Restore to format 70
log_debug "Restoring project format to 70 after Capacitor sync..."
if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
fi
log_success "Capacitor sync completed successfully"
else
# Format is not 70, run sync normally
log_debug "Project format is $current_version, running Capacitor sync normally"
if ! npx cap sync ios; then
log_error "Capacitor sync failed"
return 1
fi
log_success "Capacitor sync completed successfully"
fi
}
safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
# Step 7: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
# Step 8: Build iOS app
safe_execute "Building iOS app" "build_ios_app" || exit 5
# Step 9: Build IPA/App if requested
if [ "$BUILD_IPA" = true ]; then
log_info "Building IPA package..."
cd ios/App
xcodebuild -workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath build/App.xcarchive \
archive \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO
xcodebuild -exportArchive \
-archivePath build/App.xcarchive \
-exportPath build/ \
-exportOptionsPlist exportOptions.plist
cd ../..
log_success "IPA package built successfully"
fi
if [ "$BUILD_APP" = true ]; then
log_info "Building app bundle..."
# App bundle is already built in step 7
log_success "App bundle built successfully"
fi
# Step 10: Auto-run app if requested
if [ "$AUTO_RUN" = true ]; then
safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9
fi
# Step 11: Open Xcode if requested
if [ "$OPEN_STUDIO" = true ]; then
safe_execute "Opening Xcode" "npx cap open ios" || exit 8
fi
# Print build summary
log_success "iOS build completed successfully!"
log_info "Build mode: $BUILD_MODE"
log_info "Build type: $BUILD_TYPE"
if [ "$BUILD_IPA" = true ]; then
log_info "IPA build: completed"
fi
if [ "$BUILD_APP" = true ]; then
log_info "App build: completed"
fi
if [ "$AUTO_RUN" = true ]; then
log_info "Auto-run: completed"
fi
if [ "$OPEN_STUDIO" = true ]; then
log_info "Xcode: opened"
fi
print_footer "iOS Build"
# Exit with success
exit 0