25 KiB
iOS Migration Guide - Nilai Clock Client
Overview
This guide provides a comprehensive step-by-step process for migrating the Nilai Clock Client from Android-only to support iOS. The migration involves platform-specific implementations for background location tracking, security features, and native iOS integrations.
Prerequisites
Development Environment
- macOS: Required for iOS development
- Xcode 14+: Latest version recommended
- iOS Simulator: For testing
- Apple Developer Account: For device testing and App Store deployment
- Node.js 18+: Current project requirement
- Capacitor CLI: Already installed in project
Project Knowledge
- Understanding of current Android implementation
- Familiarity with Capacitor plugin architecture
- Knowledge of iOS development basics
- Understanding of iOS permissions and background modes
Phase 1: Environment Setup
Step 1.1: Install iOS Platform
# Navigate to project directory
cd Nilai_Clock_Client
# Add iOS platform to Capacitor
npm install @capacitor/ios
npx cap add ios
# Verify iOS platform added
ls -la ios/
Step 1.2: Initial iOS Configuration
# Copy web assets to iOS
npx cap copy ios
# Sync Capacitor plugins with iOS
npx cap sync ios
# Open project in Xcode
npx cap open ios
Step 1.3: Configure Xcode Project
- Open in Xcode: Project should open automatically
- Set Team: Select your Apple Developer team
- Bundle Identifier: Verify
com.ouji.factory.myapp - Deployment Target: Set to iOS 13.0 minimum
- Signing: Configure automatic signing
Step 1.4: Update Capacitor Configuration
// capacitor.config.json - Add iOS section
{
"appId": "com.ouji.factory.myapp",
"appName": "nilai-clock",
"webDir": "dist",
"android": {
"useLegacyBridge": true
},
"ios": {
"scheme": "NilaiClock",
"contentInset": "automatic",
"backgroundColor": "#2563eb"
},
"plugins": {
"SafeArea": {
"enabled": true,
"customColorsForSystemBars": true,
"statusBarColor": "#2563eb",
"statusBarContent": "light",
"navigationBarColor": "#ffffff",
"navigationBarContent": "dark",
"offset": 0
}
// ... other plugins
}
}
Phase 2: iOS Permissions and Info.plist
Step 2.1: Configure Info.plist
Create or update ios/App/App/Info.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- App Information -->
<key>CFBundleDisplayName</key>
<string>Nilai Clock</string>
<!-- Location Permissions -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to track work attendance and ensure workers are at authorized locations during work hours.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to verify your work location when clocking in/out.</string>
<!-- Camera Permission for QR Scanning -->
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes for clocking in/out.</string>
<!-- Background Modes -->
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>background-processing</string>
<string>background-fetch</string>
</array>
<!-- App Transport Security -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<!-- For production, use specific domain exceptions -->
<key>NSExceptionDomains</key>
<dict>
<key>myapp.ouji.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
<!-- Prevent App Backup of Sensitive Data -->
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<!-- Status Bar Configuration -->
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<!-- Launch Screen -->
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<!-- Supported Interface Orientations -->
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<!-- Requires Full Screen -->
<key>UIRequiresFullScreen</key>
<true/>
</dict>
</plist>
Step 2.2: Configure Entitlements
Create ios/App/App/App.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Background Modes -->
<key>com.apple.developer.location.background</key>
<true/>
<!-- Keychain Sharing -->
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.ouji.factory.myapp</string>
</array>
</dict>
</plist>
Phase 3: iOS-Specific Plugin Implementation
Step 3.1: Create iOS AppSecurity Plugin
Create directory structure:
mkdir -p ios/App/App/Plugins
Create ios/App/App/Plugins/AppSecurity.swift:
import Foundation
import Capacitor
import UIKit
@objc(AppSecurity)
public class AppSecurity: CAPPlugin {
@objc func getInstalledApps(_ call: CAPPluginCall) {
// iOS doesn't allow listing installed apps for privacy
// Alternative: Check for specific URL schemes of known spoofing apps
let suspiciousSchemes = [
"fakegps://",
"locationspoof://",
"gpscheat://",
"mockgps://"
]
var detectedSchemes: [String] = []
for scheme in suspiciousSchemes {
if let url = URL(string: scheme) {
if UIApplication.shared.canOpenURL(url) {
detectedSchemes.append(scheme)
}
}
}
let result: [String: Any] = [
"packages": [], // iOS doesn't provide package names
"detectedSchemes": detectedSchemes,
"platform": "ios"
]
call.resolve(result)
}
@objc func checkJailbreakStatus(_ call: CAPPluginCall) {
let isJailbroken = detectJailbreak()
let result: [String: Any] = [
"isJailbroken": isJailbroken,
"riskLevel": isJailbroken ? "high" : "low"
]
call.resolve(result)
}
@objc func performSecurityCheck(_ call: CAPPluginCall) {
let jailbroken = detectJailbreak()
let debuggerAttached = isDebuggerAttached()
let simulator = isSimulator()
var riskLevel = "low"
var riskFactors: [String] = []
if jailbroken {
riskLevel = "high"
riskFactors.append("jailbreak_detected")
}
if debuggerAttached {
riskLevel = "medium"
riskFactors.append("debugger_attached")
}
if simulator {
riskFactors.append("simulator_detected")
}
let result: [String: Any] = [
"riskLevel": riskLevel,
"riskFactors": riskFactors,
"deviceInfo": getDeviceInfo(),
"timestamp": ISO8601DateFormatter().string(from: Date())
]
call.resolve(result)
}
// MARK: - Private Methods
private func detectJailbreak() -> Bool {
// Check for jailbreak indicators
let jailbreakPaths = [
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/private/var/lib/apt/",
"/private/var/lib/cydia",
"/private/var/mobile/Library/SBSettings/Themes",
"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/private/var/cache/apt",
"/private/var/lib/apt",
"/private/var/lib/cydia",
"/private/var/log/syslog",
"/private/var/tmp/cydia.log",
"/Applications/Icy.app",
"/Applications/MxTube.app",
"/Applications/RockApp.app",
"/Applications/blackra1n.app",
"/Applications/SBSettings.app",
"/Applications/FakeCarrier.app",
"/Applications/WinterBoard.app",
"/Applications/IntelliScreen.app"
]
for path in jailbreakPaths {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
// Check if we can write to system directories
let testPath = "/private/test_jailbreak.txt"
do {
try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
try FileManager.default.removeItem(atPath: testPath)
return true // Should not be able to write here
} catch {
// Good, we can't write to system directories
}
// Check for suspicious URL schemes
let suspiciousSchemes = ["cydia://", "sileo://", "zbra://"]
for scheme in suspiciousSchemes {
if let url = URL(string: scheme) {
if UIApplication.shared.canOpenURL(url) {
return true
}
}
}
return false
}
private func isDebuggerAttached() -> Bool {
var info = kinfo_proc()
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var size = MemoryLayout<kinfo_proc>.stride
let result = sysctl(&mib, u_int(mib.count), &info, &size, nil, 0)
if result != 0 {
return false
}
return (info.kp_proc.p_flag & P_TRACED) != 0
}
private func isSimulator() -> Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
private func getDeviceInfo() -> [String: Any] {
let device = UIDevice.current
return [
"platform": "ios",
"model": device.model,
"systemName": device.systemName,
"systemVersion": device.systemVersion,
"name": device.name,
"isSimulator": isSimulator()
]
}
}
Step 3.2: Register Plugin in iOS
Update ios/App/App/AppDelegate.swift:
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url.
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}
Create ios/App/App/Plugins/AppSecurity.m (Objective-C bridge):
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AppSecurity, "AppSecurity",
CAP_PLUGIN_METHOD(getInstalledApps, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(checkJailbreakStatus, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(performSecurityCheck, CAPPluginReturnPromise);
)
Phase 4: Platform-Specific Code Adaptations
Step 4.1: Update Device UUID Service
Modify src/services/deviceUuidService.js to handle iOS:
// Add iOS-specific UUID generation
async generateDeviceUuid() {
try {
let baseString = ''
if (this.isNative) {
const deviceInfo = await Device.getInfo()
const deviceId = await Device.getId()
if (deviceInfo.platform === 'ios') {
// iOS-specific UUID generation
baseString = await this.generateIOSDeviceString(deviceInfo, deviceId)
} else {
// Android implementation (existing)
baseString = [
deviceInfo.platform || 'unknown',
deviceInfo.model || 'unknown',
deviceInfo.manufacturer || 'unknown',
deviceId.identifier || 'unknown',
deviceInfo.osVersion || 'unknown'
].join('-')
}
} else {
// Web fallback
baseString = [
navigator.userAgent,
navigator.language,
screen.width,
screen.height,
new Date().getTimezoneOffset()
].join('-')
}
// Generate UUID from base string using crypto
return await this.hashString(baseString)
} catch (error) {
console.error('Failed to generate device UUID:', error)
return this.generateFallbackUuid()
}
}
async generateIOSDeviceString(deviceInfo, deviceId) {
// iOS provides limited device identification
// Use a combination of available identifiers and stored values
try {
// Check for existing iOS-specific identifier
const { value: existingId } = await Preferences.get({ key: 'ios_device_persistent_id' })
if (existingId) {
return [
'ios',
deviceInfo.osVersion || 'unknown',
existingId,
deviceId.identifier || 'unknown'
].join('-')
}
// Generate new persistent identifier for iOS
const persistentId = this.generateRandomId(32)
await Preferences.set({
key: 'ios_device_persistent_id',
value: persistentId
})
return [
'ios',
deviceInfo.osVersion || 'unknown',
persistentId,
deviceId.identifier || 'unknown'
].join('-')
} catch (error) {
console.error('Failed to generate iOS device string:', error)
return `ios-fallback-${Date.now()}`
}
}
Step 4.2: Update Background Location Service
Modify src/services/backgroundLocationService.js for iOS compatibility:
// Add iOS-specific initialization
async initialize() {
if (!this.isNative) {
console.warn('Background location is only available on native platforms')
return false
}
try {
const deviceInfo = await Device.getInfo()
if (deviceInfo.platform === 'ios') {
await this.initializeIOS()
} else {
await this.initializeAndroid()
}
this.isInitialized = true
console.log(`Background location initialized for ${deviceInfo.platform}`)
return true
} catch (error) {
console.error('Failed to initialize background location:', error)
return false
}
}
async initializeIOS() {
console.log('Initializing iOS background location...')
// Request location permissions first
const permissions = await Geolocation.requestPermissions()
console.log('iOS location permissions:', permissions)
if (permissions.location !== 'granted') {
throw new Error('Location permission not granted')
}
// Initialize background geolocation with iOS-specific config
if (Capacitor.isNativePlatform()) {
const messages = this.getNotificationMessages()
try {
this.watcherId = await BackgroundGeolocation.addWatcher(
{
requestPermissions: true,
stale: false,
distanceFilter: 50,
backgroundMessage: messages.message,
backgroundTitle: messages.title,
// iOS-specific options
pausesLocationUpdatesAutomatically: false,
allowsBackgroundLocationUpdates: true,
showsBackgroundLocationIndicator: false, // Set to true for debugging
desiredAccuracy: 'high'
},
(location, error) => {
if (error) {
console.error('iOS background location error:', error)
return
}
if (location) {
console.log('iOS background location update:', location)
this.handleLocationUpdate(location)
}
}
)
console.log(`iOS BackgroundGeolocation watcher added with ID: ${this.watcherId}`)
} catch (watcherError) {
console.error('Failed to add iOS BackgroundGeolocation watcher:', watcherError)
this.watcherId = null
throw watcherError
}
}
}
// iOS-specific location permission handling
async requestIOSLocationPermissions() {
try {
// First request "when in use" permission
let permissions = await Geolocation.requestPermissions()
if (permissions.location === 'granted') {
// Try to upgrade to "always" permission
// Note: iOS requires user to manually change this in Settings
console.log('Location permission granted. User may need to change to "Always" in Settings.')
return true
}
throw new Error(`Location permission denied: ${permissions.location}`)
} catch (error) {
console.error('Failed to request iOS location permissions:', error)
throw error
}
}
Step 4.3: Update Anti-Spoofing Service
Modify src/services/antiSpoofingService.js for iOS:
// Add iOS-specific security checks
async performSecurityCheck() {
if (!this.isNative) {
console.warn('Security checks only available on native platforms')
return { riskLevel: 'unknown', platform: 'web' }
}
try {
const deviceInfo = await Device.getInfo()
if (deviceInfo.platform === 'ios') {
return await this.performIOSSecurityCheck()
} else {
return await this.performAndroidSecurityCheck()
}
} catch (error) {
console.error('Security check failed:', error)
return { riskLevel: 'unknown', error: error.message }
}
}
async performIOSSecurityCheck() {
try {
// Use custom iOS AppSecurity plugin
const securityResult = await AppSecurity.performSecurityCheck()
// Send results to server
await this.sendSecurityCheckToServer({
platform: 'ios',
...securityResult
})
// Handle high-risk scenarios
if (securityResult.riskLevel === 'high') {
await this.handleSecurityViolation('ios_security_risk', securityResult)
}
return securityResult
} catch (error) {
console.error('iOS security check failed:', error)
return { riskLevel: 'unknown', error: error.message }
}
}
async handleIOSJailbreakDetection(isJailbroken) {
if (isJailbroken) {
console.warn('iOS jailbreak detected!')
// Log security alert
await this.sendSecurityAlert({
alertType: 'ios_jailbreak_detected',
severity: 'high',
data: {
platform: 'ios',
timestamp: new Date().toISOString()
}
})
// Optionally block app functionality
this.dispatchSecurityEvent('app-blocked', {
message: 'Security violation detected. Please contact your administrator.'
})
}
}
Phase 5: Testing and Validation
Step 5.1: iOS Simulator Testing
# Build and run on iOS simulator
npm run build
npx cap copy ios
npx cap sync ios
npx cap open ios
# In Xcode:
# 1. Select iOS Simulator
# 2. Choose device (iPhone 14, iPad, etc.)
# 3. Click Run button
Step 5.2: iOS Device Testing
- Connect iOS Device: Use USB cable
- Trust Computer: On device when prompted
- Select Device: In Xcode device list
- Run on Device: Click Run button in Xcode
Step 5.3: iOS Testing Checklist
Location Services
- Location permission request works
- "When in Use" permission granted
- Background location tracking starts
- Location updates received in background
- Geofence validation works
- Location accuracy acceptable
Camera/QR Scanning
- Camera permission request works
- QR scanner opens camera
- QR codes scan successfully
- Camera closes after scan
- Error handling for permission denial
Security Features
- Jailbreak detection works
- Security checks complete
- Risk assessment accurate
- Security alerts sent to server
App Lifecycle
- App launches successfully
- Background/foreground transitions
- App state preservation
- Memory management
Data Persistence
- Secure storage works (Keychain)
- Data survives app restart
- Authentication tokens persist
- User preferences saved
Network Connectivity
- API calls work over WiFi
- API calls work over cellular
- Offline mode handling
- Network error recovery
Step 5.4: iOS-Specific Issues and Solutions
Common iOS Issues
Issue: Background location stops working
// Solution: Ensure proper background modes in Info.plist
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>background-processing</string>
</array>
Issue: Camera permission denied
// Solution: Check permission status and guide user
const checkCameraPermission = async () => {
const permissions = await Camera.requestPermissions()
if (permissions.camera === 'denied') {
// Guide user to Settings app
alert('Please enable camera access in Settings > Privacy > Camera')
}
}
Issue: App crashes on device but works in simulator
# Solution: Check device logs in Xcode
# Window > Devices and Simulators > Select Device > View Device Logs
Issue: Location permission downgraded from "Always" to "When in Use"
// Solution: Monitor permission changes and re-request
const monitorLocationPermissions = async () => {
const permissions = await Geolocation.checkPermissions()
if (permissions.location === 'granted-when-in-use') {
// Inform user to change to "Always" in Settings
showLocationPermissionAlert()
}
}
Phase 6: iOS Deployment
Step 6.1: App Store Preparation
- Apple Developer Account: Ensure active membership
- App Store Connect: Create app record
- Certificates: Generate distribution certificate
- Provisioning Profile: Create App Store profile
- App Icons: Prepare all required sizes
- Screenshots: Capture for all device sizes
Step 6.2: Build for Distribution
# Production build
npm run build:production
npx cap copy ios
npx cap sync ios
# Open Xcode for archiving
npx cap open ios
In Xcode:
- Select "Any iOS Device" as target
- Product > Archive to create archive
- Distribute App when archive completes
- Upload to App Store Connect
Step 6.3: App Store Review Preparation
Privacy Policy Requirements
- Location usage justification
- Camera usage explanation
- Data collection practices
- Third-party service usage
App Review Information
Location Usage: This app tracks worker location during work hours to verify attendance at authorized work sites. Location data is used only for work verification and is not shared with third parties.
Camera Usage: The app uses the camera to scan QR codes for clocking in/out at work locations. No photos are stored or transmitted.
Background Location: Required to ensure workers remain at authorized locations during work hours and for accurate time tracking.
Step 6.4: iOS Deployment Checklist
Pre-Submission
- All iOS testing completed
- App icons and metadata ready
- Privacy policy updated
- App Store screenshots captured
- Age rating determined
- Keywords and description optimized
Technical Requirements
- iOS 13.0+ compatibility verified
- All device sizes tested
- Performance optimization completed
- Memory usage optimized
- Battery usage minimized
App Store Guidelines
- Location usage clearly justified
- No unnecessary permissions requested
- User data protection implemented
- Accessibility features supported
- Content guidelines followed
Conclusion
The iOS migration requires careful attention to platform-specific implementations, particularly for background location services and security features. The modular architecture of the current Android implementation makes the migration feasible, with most business logic remaining unchanged while native services require iOS-specific implementations.
Key success factors:
- Proper iOS permissions and background modes configuration
- Platform-specific security implementations
- Thorough testing on iOS devices and simulators
- App Store compliance and review preparation
- User experience optimization for iOS patterns
The migration timeline should allow for 4-6 weeks of development and testing, followed by 1-2 weeks for App Store review and approval.