2019-07-30 16:54:44 -07:00
//
// L a u n c h V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 7 / 3 0 / 1 9 .
// C o p y r i g h t © 2 0 1 9 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import UIKit
import Roxas
2022-11-02 17:58:59 -07:00
import minimuxer
2023-09-08 15:04:03 -05:00
import WidgetKit
2025-11-07 16:52:55 -05:00
import AltSign
2020-09-03 16:39:08 -07:00
import AltStoreCore
2022-11-16 13:14:04 -07:00
import UniformTypeIdentifiers
2020-09-03 16:39:08 -07:00
2023-04-01 16:02:12 -07:00
let pairingFileName = " ALTPairingFile.mobiledevicepairing "
2025-10-15 20:22:35 +05:30
final class LaunchViewController : UIViewController , UIDocumentPickerDelegate {
2019-11-04 12:32:36 -08:00
private var didFinishLaunching = false
2025-10-15 20:22:35 +05:30
private var retries = 0
private var maxRetries = 3
private var splashView : SplashView !
private var destinationViewController : TabBarController ?
private var startTime : Date !
2019-07-30 16:54:44 -07:00
2025-10-15 20:22:35 +05:30
override func viewDidLoad ( ) {
2020-01-13 13:53:04 -08:00
super . viewDidLoad ( )
2025-10-15 20:22:35 +05:30
splashView = SplashView ( frame : view . bounds , appName : " SideStore " )
destinationViewController = storyboard ! . instantiateViewController ( withIdentifier : " tabBarController " ) as ? TabBarController
view . addSubview ( splashView )
2022-11-13 17:40:33 -08:00
}
2025-10-15 20:22:35 +05:30
2022-11-13 17:40:33 -08:00
override func viewDidAppear ( _ animated : Bool ) {
2025-10-15 20:22:35 +05:30
super . viewDidAppear ( animated )
guard ! didFinishLaunching else { return }
Task {
startTime = Date ( )
await runLaunchSequence ( )
doPostLaunch ( )
}
}
private func runLaunchSequence ( ) async {
guard retries < maxRetries else { return }
retries += 1
await Task . detached {
if ! DatabaseManager . shared . isStarted {
await withCheckedContinuation { continuation in
DatabaseManager . shared . start { error in
if let error {
Task { await self . handleLaunchError ( error , retryCallback : self . runLaunchSequence ) }
} else {
Task { await self . finishLaunching ( ) }
2024-06-17 09:43:25 +10:00
}
2025-10-15 20:22:35 +05:30
continuation . resume ( returning : ( ) )
2024-06-17 09:43:25 +10:00
}
}
2025-10-15 20:22:35 +05:30
} else {
await self . finishLaunching ( )
2024-06-17 09:43:25 +10:00
}
2025-10-15 20:22:35 +05:30
} . value
}
private func doPostLaunch ( ) {
SideJITManager . shared . checkAndPromptIfNeeded ( presentingVC : self )
2024-06-17 09:43:25 +10:00
if #available ( iOS 17 , * ) , UserDefaults . standard . sidejitenable {
2025-10-15 20:22:35 +05:30
DispatchQueue . global ( ) . async { SideJITManager . shared . askForNetwork ( ) }
2024-06-17 09:43:25 +10:00
print ( " SideJITServer Enabled " )
}
2025-10-15 20:22:35 +05:30
2023-01-04 09:32:04 -05:00
#if ! targetEnvironment ( simulator )
2025-11-07 16:52:55 -05:00
detectAndImportAccountFile ( )
guard let pf = fetchPairingFile ( ) else {
2022-11-13 17:40:33 -08:00
displayError ( " Device pairing file not found. " )
2022-11-08 14:15:09 -05:00
return
}
2022-11-16 13:14:04 -07:00
start_minimuxer_threads ( pf )
2023-01-04 09:32:04 -05:00
#endif
2022-11-08 14:15:09 -05:00
}
2024-07-12 02:40:26 -07:00
2025-10-15 20:22:35 +05:30
func start_minimuxer_threads ( _ pairing_file : String ) {
target_minimuxer_address ( )
let documentsDirectory = FileManager . default . documentsDirectory . absoluteString
do {
let loggingEnabled = UserDefaults . standard . isMinimuxerConsoleLoggingEnabled
try minimuxer . startWithLogger ( pairing_file , documentsDirectory , loggingEnabled )
} catch {
try ! FileManager . default . removeItem ( at : FileManager . default . documentsDirectory . appendingPathComponent ( pairingFileName ) )
displayError ( " minimuxer failed to start, please restart SideStore. \( ( error as ? LocalizedError ) ? . failureReason ? ? " UNKNOWN ERROR " ) " )
2022-11-08 14:15:09 -05:00
}
2025-10-15 20:22:35 +05:30
start_auto_mounter ( documentsDirectory )
2022-11-08 14:15:09 -05:00
}
2022-11-16 16:41:02 -05:00
2025-10-15 20:22:35 +05:30
func fetchPairingFile ( ) -> String ? { PairingFileManager . shared . fetchPairingFile ( presentingVC : self ) }
2022-11-08 14:15:09 -05:00
func displayError ( _ msg : String ) {
2022-11-13 17:40:33 -08:00
print ( msg )
2025-10-15 20:22:35 +05:30
let alert = UIAlertController ( title : " Error launching SideStore " , message : msg , preferredStyle : . alert )
self . present ( alert , animated : true )
2020-01-13 13:53:04 -08:00
}
2025-10-15 20:22:35 +05:30
2022-11-16 13:14:04 -07:00
func documentPicker ( _ controller : UIDocumentPickerViewController , didPickDocumentsAt urls : [ URL ] ) {
let url = urls [ 0 ]
let isSecuredURL = url . startAccessingSecurityScopedResource ( ) = = true
2025-11-07 16:52:55 -05:00
defer {
if ( isSecuredURL ) {
url . stopAccessingSecurityScopedResource ( )
}
}
2022-11-16 13:14:04 -07:00
do {
2025-10-15 20:22:35 +05:30
let data = try Data ( contentsOf : url )
guard let pairingString = String ( data : data , encoding : . utf8 ) else {
2022-11-16 13:14:04 -07:00
displayError ( " Unable to read pairing file " )
2025-10-15 20:22:35 +05:30
return
2022-11-16 13:14:04 -07:00
}
2025-10-15 20:22:35 +05:30
try pairingString . write ( to : FileManager . default . documentsDirectory . appendingPathComponent ( pairingFileName ) , atomically : true , encoding : . utf8 )
start_minimuxer_threads ( pairingString )
2022-11-16 13:14:04 -07:00
} catch {
displayError ( " Unable to read pairing file " )
}
2025-11-07 16:52:55 -05:00
controller . dismiss ( animated : true , completion : nil )
2022-11-16 13:14:04 -07:00
}
2025-10-15 20:22:35 +05:30
2022-11-16 13:14:04 -07:00
func documentPickerWasCancelled ( _ controller : UIDocumentPickerViewController ) {
2022-12-17 21:13:53 +00:00
displayError ( " Choosing a pairing file was cancelled. Please re-open the app and try again. " )
2022-11-16 13:14:04 -07:00
}
2025-11-07 16:52:55 -05:00
func importAccountAtFile ( _ file : URL , remove : Bool = false ) {
_ = file . startAccessingSecurityScopedResource ( )
defer { file . stopAccessingSecurityScopedResource ( ) }
guard let accountD = try ? Data ( contentsOf : file ) else {
let toastView = ToastView ( text : NSLocalizedString ( " Could not read data from file! " , comment : " " ) , detailText : " \( file ) " )
return toastView . show ( in : self )
}
guard let account = try ? Foundation . JSONDecoder ( ) . decode ( ImportedAccount . self , from : accountD ) else {
let toastView = ToastView ( text : NSLocalizedString ( " Could not parse data from file! " , comment : " " ) , detailText : " \( file ) " )
return toastView . show ( in : self )
}
print ( " We want to import this account probably: \( account ) " )
if remove {
try ? FileManager . default . removeItem ( at : file )
}
Keychain . shared . appleIDEmailAddress = account . email
Keychain . shared . appleIDPassword = account . password
Keychain . shared . adiPb = account . adiPB
Keychain . shared . identifier = account . local_user
if let altCert = ALTCertificate ( p12Data : account . cert , password : account . certpass ) {
Keychain . shared . signingCertificate = altCert . encryptedP12Data ( withPassword : " " ) !
Keychain . shared . signingCertificatePassword = account . certpass
let toastView = ToastView ( text : NSLocalizedString ( " Successfully imported ' \( account . email ) '! " , comment : " " ) , detailText : " SideStore should be fully operational! " )
return toastView . show ( in : self )
} else {
let toastView = ToastView ( text : NSLocalizedString ( " Failed to import account certificate! " , comment : " " ) , detailText : " Failed to create ALTCertificate. Check if the password is correct. Still imported account/adi.pb details! " )
return toastView . show ( in : self )
}
}
func detectAndImportAccountFile ( ) {
let accountFileURL = FileManager . default . documentsDirectory . appendingPathComponent ( " Account.sideconf " )
#if ! DEBUG
importAccountAtFile ( accountFileURL , remove : true )
#else
importAccountAtFile ( accountFileURL )
#endif
}
2019-07-30 16:54:44 -07:00
}
2025-10-15 20:22:35 +05:30
extension LaunchViewController {
@ MainActor
func handleLaunchError ( _ error : Error , retryCallback : ( ( ) async -> Void ) ? = nil ) {
do { throw error } catch let error as NSError {
2022-11-05 23:50:07 -07:00
let title = error . userInfo [ NSLocalizedFailureErrorKey ] as ? String ? ? NSLocalizedString ( " Unable to Launch SideStore " , comment : " " )
2025-10-15 20:22:35 +05:30
let desc : String
if #available ( iOS 14.5 , * ) {
desc = ( [ error . debugDescription ] + error . underlyingErrors . map { ( $0 as NSError ) . debugDescription } ) . joined ( separator : " \n \n " )
} else {
desc = error . debugDescription
2022-09-20 13:19:17 -05:00
}
2025-10-15 20:22:35 +05:30
let alert = UIAlertController ( title : title , message : desc , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : NSLocalizedString ( " Retry " , comment : " " ) , style : . default ) { _ in
Task { await retryCallback ? ( ) }
} )
present ( alert , animated : true )
2019-07-30 16:54:44 -07:00
}
}
2025-10-15 20:22:35 +05:30
@ MainActor
func finishLaunching ( ) async {
guard ! didFinishLaunching else { return }
didFinishLaunching = true
2019-11-04 12:32:36 -08:00
2019-07-30 16:54:44 -07:00
AppManager . shared . update ( )
2023-05-16 15:46:37 -05:00
AppManager . shared . updatePatronsIfNeeded ( )
2019-08-28 11:13:22 -07:00
PatreonAPI . shared . refreshPatreonAccount ( )
2023-12-07 17:30:46 -06:00
AppManager . shared . updateAllSources { result in
guard case . failure ( let error ) = result else { return }
Logger . main . error ( " Failed to update sources on launch. \( error . localizedDescription , privacy : . public ) " )
2024-02-15 19:31:11 -06:00
2025-10-15 20:22:35 +05:30
2025-01-20 23:03:45 +05:30
let errorDesc = ErrorProcessing ( . fullError ) . getDescription ( error : error as NSError )
print ( " Failed to update sources on launch. \( errorDesc ) " )
2025-02-08 04:45:22 +05:30
var mode : ToastView . InfoMode = . fullError
if String ( describing : error ) . contains ( " The Internet connection appears to be offline " ) {
mode = . localizedDescription // d o n t m a k e n o i s e !
}
let toastView = ToastView ( error : error , mode : mode )
2024-02-15 19:31:11 -06:00
toastView . addTarget ( self . destinationViewController , action : #selector ( TabBarController . presentSources ) , for : . touchUpInside )
2025-10-15 20:22:35 +05:30
toastView . show ( in : self . destinationViewController ! . selectedViewController ? ? self . destinationViewController ! )
2023-12-07 17:30:46 -06:00
}
2025-10-15 20:22:35 +05:30
updateKnownSources ( )
WidgetCenter . shared . reloadAllTimelines ( )
didFinishLaunching = true
2023-12-07 17:30:46 -06:00
2025-10-15 20:22:35 +05:30
let destinationVC = destinationViewController !
2023-05-16 15:46:37 -05:00
2025-10-15 20:22:35 +05:30
let elapsed = abs ( startTime . timeIntervalSinceNow )
let remaining = elapsed >= 1 ? 0 : 1 - elapsed
try ? await Task . sleep ( nanoseconds : UInt64 ( remaining * 1_000_000_000 ) )
2023-09-08 15:04:03 -05:00
2025-10-15 20:22:35 +05:30
destinationVC . loadViewIfNeeded ( )
addChild ( destinationVC )
destinationVC . view . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( destinationVC . view )
destinationVC . didMove ( toParent : self )
2019-11-04 12:32:36 -08:00
2025-10-15 20:22:35 +05:30
// P i n e d g e s B E F O R E a n i m a t i o n
NSLayoutConstraint . activate ( [
destinationVC . view . topAnchor . constraint ( equalTo : view . topAnchor ) ,
destinationVC . view . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
destinationVC . view . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
destinationVC . view . trailingAnchor . constraint ( equalTo : view . trailingAnchor )
] )
// S e t i n i t i a l a l p h a f o r f a d e - i n
destinationVC . view . alpha = 0
UIView . transition ( with : view , duration : 0.3 , options : . transitionCrossDissolve ) { [ self ] in
self . splashView . alpha = 0
destinationVC . view . alpha = 1
} completion : { _ in
self . splashView . removeFromSuperview ( )
self . destinationViewController = destinationVC
2019-11-04 12:32:36 -08:00
}
2019-07-30 16:54:44 -07:00
}
2023-05-16 15:46:37 -05:00
2025-10-15 20:22:35 +05:30
func updateKnownSources ( ) {
2023-05-16 15:46:37 -05:00
AppManager . shared . updateKnownSources { result in
2025-10-15 20:22:35 +05:30
switch result {
2023-05-16 15:46:37 -05:00
case . failure ( let error ) : print ( " [ALTLog] Failed to update known sources: " , error )
case . success ( ( _ , let blockedSources ) ) :
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
let blockedSourceIDs = Set ( blockedSources . lazy . map { $0 . identifier } )
let blockedSourceURLs = Set ( blockedSources . lazy . compactMap { $0 . sourceURL } )
2025-10-15 20:22:35 +05:30
let predicate = NSPredicate ( format : " %K IN %@ OR %K IN %@ " , # keyPath ( Source . identifier ) , blockedSourceIDs , # keyPath ( Source . sourceURL ) , blockedSourceURLs )
let sourceErrors = Source . all ( satisfying : predicate , in : context ) . map { source in
let blocked = blockedSources . first { $0 . identifier = = source . identifier }
return SourceError . blocked ( source , bundleIDs : blocked ? . bundleIDs , existingSource : source )
2023-05-16 15:46:37 -05:00
}
guard ! sourceErrors . isEmpty else { return }
Task {
2025-10-15 20:22:35 +05:30
for error in sourceErrors {
2023-05-16 15:46:37 -05:00
let title = String ( format : NSLocalizedString ( " “%@” Blocked " , comment : " " ) , error . $ source . name )
let message = [ error . localizedDescription , error . recoverySuggestion ] . compactMap { $0 } . joined ( separator : " \n \n " )
await self . presentAlert ( title : title , message : message )
}
}
}
}
}
}
}
2025-10-15 20:22:35 +05:30
// MARK: - S p l a s h V i e w
final class SplashView : UIView {
let iconView = UIImageView ( )
let titleLabel = UILabel ( )
init ( frame : CGRect , appName : String ) {
super . init ( frame : frame )
backgroundColor = . systemBackground
setupIcon ( )
setupTitle ( appName : appName )
}
required init ? ( coder : NSCoder ) { fatalError ( ) }
private func setupIcon ( ) {
let container = UIView ( )
container . translatesAutoresizingMaskIntoConstraints = false
container . layer . shadowColor = UIColor . black . cgColor
container . layer . shadowOpacity = 0.25
container . layer . shadowOffset = CGSize ( width : 0 , height : 4 )
container . layer . shadowRadius = 8
addSubview ( container )
iconView . image = UIImage ( named : " AppIcon " ) ? ? UIImage ( named : " AppIcon60x60 " ) ? ? UIImage ( systemName : " app.fill " )
iconView . contentMode = . scaleAspectFit
iconView . translatesAutoresizingMaskIntoConstraints = false
iconView . layer . cornerRadius = 24
iconView . clipsToBounds = true
container . addSubview ( iconView )
NSLayoutConstraint . activate ( [
container . centerXAnchor . constraint ( equalTo : centerXAnchor ) ,
container . centerYAnchor . constraint ( equalTo : centerYAnchor , constant : - 20 ) ,
container . widthAnchor . constraint ( equalToConstant : 120 ) ,
container . heightAnchor . constraint ( equalToConstant : 120 ) ,
iconView . topAnchor . constraint ( equalTo : container . topAnchor ) ,
iconView . bottomAnchor . constraint ( equalTo : container . bottomAnchor ) ,
iconView . leadingAnchor . constraint ( equalTo : container . leadingAnchor ) ,
iconView . trailingAnchor . constraint ( equalTo : container . trailingAnchor )
] )
}
private func setupTitle ( appName : String ) {
titleLabel . text = appName
titleLabel . font = . systemFont ( ofSize : 24 , weight : . bold )
titleLabel . textColor = . label
titleLabel . textAlignment = . center
titleLabel . translatesAutoresizingMaskIntoConstraints = false
addSubview ( titleLabel )
NSLayoutConstraint . activate ( [
titleLabel . topAnchor . constraint ( equalTo : iconView . bottomAnchor , constant : 12 ) ,
titleLabel . centerXAnchor . constraint ( equalTo : centerXAnchor )
] )
}
}
// MARK: - P a i r i n g F i l e M a n a g e r
final class PairingFileManager {
static let shared = PairingFileManager ( )
func fetchPairingFile ( presentingVC : UIViewController ) -> String ? {
let fm = FileManager . default
let filename = pairingFileName
let documentsPath = fm . documentsDirectory . appendingPathComponent ( " / \( filename ) " )
if fm . fileExists ( atPath : documentsPath . path ) ,
let contents = try ? String ( contentsOf : documentsPath ) , ! contents . isEmpty {
return contents
}
if let url = Bundle . main . url ( forResource : " ALTPairingFile " , withExtension : " mobiledevicepairing " ) ,
fm . fileExists ( atPath : url . path ) ,
let data = fm . contents ( atPath : url . path ) ,
let contents = String ( data : data , encoding : . utf8 ) ,
! contents . isEmpty , ! UserDefaults . standard . isPairingReset { return contents }
if let plistString = Bundle . main . object ( forInfoDictionaryKey : " ALTPairingFile " ) as ? String ,
! plistString . isEmpty , ! plistString . contains ( " insert pairing file here " ) , ! UserDefaults . standard . isPairingReset { return plistString }
presentPairingFileAlert ( on : presentingVC )
return nil
}
private func presentPairingFileAlert ( on vc : UIViewController ) {
let alert = UIAlertController ( title : " Pairing File " , message : " Select the pairing file or select \" Help \" for help. " , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : " Help " , style : . default ) { _ in
if let url = URL ( string : " https://docs.sidestore.io/docs/installation/pairing-file " ) { UIApplication . shared . open ( url ) }
sleep ( 2 ) ; exit ( 0 )
} )
alert . addAction ( UIAlertAction ( title : " OK " , style : . default ) { _ in
var types = UTType . types ( tag : " plist " , tagClass : . filenameExtension , conformingTo : nil )
types . append ( contentsOf : UTType . types ( tag : " mobiledevicepairing " , tagClass : . filenameExtension , conformingTo : . data ) )
types . append ( . xml )
let picker = UIDocumentPickerViewController ( forOpeningContentTypes : types )
picker . delegate = vc as ? UIDocumentPickerDelegate
picker . shouldShowFileExtensions = true
vc . present ( picker , animated : true )
UserDefaults . standard . isPairingReset = false
} )
vc . present ( alert , animated : true )
}
}
// MARK: - S i d e J I T M a n a g e r
final class SideJITManager {
static let shared = SideJITManager ( )
func checkAndPromptIfNeeded ( presentingVC : UIViewController ) {
guard #available ( iOS 17 , * ) , ! UserDefaults . standard . sidejitenable else { return }
DispatchQueue . global ( ) . async {
self . isSideJITServerDetected { result in
DispatchQueue . main . async {
switch result {
case . success ( ) :
let alert = UIAlertController ( title : " SideJITServer Detected " , message : " Would you like to enable SideJITServer " , preferredStyle : . alert )
alert . addAction ( UIAlertAction ( title : " OK " , style : . default ) { _ in UserDefaults . standard . sidejitenable = true } )
alert . addAction ( UIAlertAction ( title : " Cancel " , style : . cancel ) )
presentingVC . present ( alert , animated : true )
case . failure ( _ ) : print ( " Cannot find sideJITServer " )
}
}
}
}
}
func askForNetwork ( ) {
let address = UserDefaults . standard . textInputSideJITServerurl ? ? " "
let SJSURL = address . isEmpty ? " http://sidejitserver._http._tcp.local:8080 " : address
URLSession . shared . dataTask ( with : URL ( string : " \( SJSURL ) /re/ " ) ! ) { data , resp , err in
print ( " data: \( String ( describing : data ) ) , response: \( String ( describing : resp ) ) , error: \( String ( describing : err ) ) " )
} . resume ( )
}
func isSideJITServerDetected ( completion : @ escaping ( Result < Void , Error > ) -> Void ) {
let address = UserDefaults . standard . textInputSideJITServerurl ? ? " "
let SJSURL = address . isEmpty ? " http://sidejitserver._http._tcp.local:8080 " : address
guard let url = URL ( string : SJSURL ) else { return }
URLSession . shared . dataTask ( with : url ) { _ , _ , error in
if let error = error { completion ( . failure ( error ) ) ; return }
completion ( . success ( ( ) ) )
} . resume ( )
}
}