2023-05-19 13:14:15 +02:00
//
// O n b o a r d i n g V i e w . s w i f t
// S i d e S t o r e
//
// C r e a t e d b y F a b i a n T h i e s o n 2 5 . 0 2 . 2 3 .
// C o p y r i g h t © 2 0 2 3 S i d e S t o r e . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
import CoreData
import AltStoreCore
import minimuxer
import Reachability
import UniformTypeIdentifiers
2023-05-20 22:14:50 +02:00
enum OnboardingStep : Int , CaseIterable {
case welcome , pairing , wireguard , wireguardConfig , addSources , finish
}
2023-05-19 13:14:15 +02:00
struct OnboardingView : View {
@ Environment ( \ . dismiss ) var dismiss
// T e m p o r a r y w o r k a r o u n d f o r U I K i t c o m p a t i b i l i t y
var onDismiss : ( ( ) -> Void ) ? = nil
2023-05-20 22:14:50 +02:00
var enabledSteps = OnboardingStep . allCases
@ State private var currentStep : OnboardingStep = . welcome
2023-05-19 13:14:15 +02:00
@ State private var pairingFileURL : URL ? = nil
@ State private var isWireGuardAppStorePageVisible : Bool = false
@ State private var isDownloadingWireGuardProfile : Bool = false
@ State private var wireGuardProfileFileURL : URL ? = nil
@ State private var reachabilityNotifier : Reachability ? = nil
@ State private var isWireGuardTunnelReachable : Bool = false
@ State private var areTrustedSourcesEnabled : Bool = false
@ State private var isLoadingTrustedSources : Bool = false
let pairingFileTypes = UTType . types ( tag : " plist " , tagClass : UTTagClass . filenameExtension , conformingTo : nil ) + UTType . types ( tag : " mobiledevicepairing " , tagClass : UTTagClass . filenameExtension , conformingTo : UTType . data ) + [ . xml ]
var body : some View {
TabView ( selection : self . $ currentStep ) {
2023-05-20 22:14:50 +02:00
ForEach ( self . enabledSteps , id : \ . self ) { step in
self . viewForStep ( step )
. tag ( step )
// H a c k t o d i s a b l e h o r i z o n t a l s c r o l l i n g i n o n b o a r d i n g s c r e e n s
. background (
Color . black
. opacity ( 0.001 )
. edgesIgnoringSafeArea ( . all )
)
. highPriorityGesture ( DragGesture ( ) )
}
2023-05-19 13:14:15 +02:00
}
. tabViewStyle ( PageTabViewStyle ( indexDisplayMode : . never ) )
. edgesIgnoringSafeArea ( . bottom )
2023-05-20 22:14:50 +02:00
. background (
Color . accentColor
. opacity ( 0.1 )
. edgesIgnoringSafeArea ( . all )
)
2023-05-19 13:14:15 +02:00
. onChange ( of : self . currentStep ) { step in
switch step {
case . wireguardConfig :
self . startPingingWireGuardTunnel ( )
default :
self . stopPingingWireGuardTunnel ( )
}
}
}
var welcomeStep : some View {
OnboardingStepView {
VStack ( alignment : . leading ) {
Text ( " Welcome to " )
Text ( " SideStore " )
. foregroundColor ( . accentColor )
}
} hero : {
AppIconsShowcase ( )
} content : {
VStack ( alignment : . leading , spacing : 16 ) {
Text ( " Before you can start sideloading apps, there is some setup to do. " )
Text ( " The following setup will guide you through the steps one by one. " )
Text ( " You will need a computer (Windows, macOS, Linux) and your Apple ID. " )
}
} action : {
SwiftUI . Button ( " Continue " ) {
self . showNextStep ( )
}
. buttonStyle ( FilledButtonStyle ( ) )
}
}
var pairingView : some View {
OnboardingStepView ( title : {
VStack ( alignment : . leading ) {
Text ( " Pair your Device " )
}
} , hero : {
Image ( systemSymbol : . link )
. resizable ( )
. aspectRatio ( contentMode : . fit )
. foregroundColor ( . accentColor )
. shadow ( color : . accentColor . opacity ( 0.8 ) , radius : 12 )
} , content : {
VStack ( alignment : . leading , spacing : 16 ) {
2023-05-20 22:14:50 +02:00
Text ( " SideStore supports on-device sideloading even on non-jailbroken devices. " )
2023-05-19 13:14:15 +02:00
Text ( " For it to work, you have to generate a pairing file as described [here in our documentation](https://wiki.sidestore.io/guides/install#pairing-process). " )
Text ( " Once you have the `<UUID>.mobiledevicepairing`, import it using the button below. " )
}
} , action : {
ModalNavigationLink ( " Select Pairing File " ) {
DocumentPicker ( selectedUrl : self . $ pairingFileURL ,
supportedTypes : self . pairingFileTypes . map { $0 . identifier } )
}
. buttonStyle ( FilledButtonStyle ( ) )
. onChange ( of : self . pairingFileURL ) { newValue in
guard let url = newValue else {
return
}
self . importPairingFile ( url : url )
}
} )
}
var wireguardView : some View {
OnboardingStepView ( title : {
VStack ( alignment : . leading ) {
Text ( " Download WireGuard " )
}
} , hero : {
Image ( systemSymbol : . icloudAndArrowDown )
. resizable ( )
. aspectRatio ( contentMode : . fit )
. foregroundColor ( . accentColor )
. shadow ( color : . accentColor . opacity ( 0.8 ) , radius : 12 )
} , content : {
VStack ( alignment : . leading , spacing : 16 ) {
Text ( " To sideload and sign app on-device without the need of a computer program like SideServer, a local WireGuard connection is required. " )
Text ( " This connection is strictly local-only and does not connect to a server on the internet. " )
Text ( " First, download WireGuard from the App Store (free). " )
}
} , action : {
AppStoreView ( isVisible : self . $ isWireGuardAppStorePageVisible , itunesItemId : 1441195209 )
. frame ( width : . zero , height : . zero )
VStack {
SwiftUI . Button ( " Show in App Store " ) {
self . isWireGuardAppStorePageVisible = true
}
. buttonStyle ( FilledButtonStyle ( ) )
SwiftUI . Button ( " Continue " ) {
self . showNextStep ( )
}
. buttonStyle ( FilledButtonStyle ( ) )
}
} )
}
var wireguardConfigView : some View {
OnboardingStepView ( title : {
VStack ( alignment : . leading ) {
Text ( " Enable the WireGuard Tunnel " )
}
} , hero : {
Image ( systemSymbol : . network )
. resizable ( )
. aspectRatio ( contentMode : . fit )
. foregroundColor ( . accentColor )
. shadow ( color : . accentColor . opacity ( 0.8 ) , radius : 12 )
} , content : {
VStack ( alignment : . leading , spacing : 16 ) {
Text ( " Once WireGuard is installed, a configuration file has to be installed in the WireGuard app. " )
Text ( " Tap the button below and open the downloaded file in the WireGuard app. " )
Text ( " Then, activate the VPN tunnel to continue. " )
}
} , action : {
VStack {
SwiftUI . Button ( " Download and Install Configuration File " ) {
self . downloadWireGuardProfile ( )
}
. buttonStyle ( FilledButtonStyle ( isLoading : self . isDownloadingWireGuardProfile ) )
. sheet ( item : self . $ wireGuardProfileFileURL ) { fileURL in
ActivityView ( items : [ fileURL ] )
}
SwiftUI . Button ( self . isWireGuardTunnelReachable ? " Continue " : " Waiting for connection... " ,
action : self . showNextStep )
. buttonStyle ( FilledButtonStyle ( ) )
. disabled ( ! self . isWireGuardTunnelReachable )
}
} )
}
var addSourcesView : some View {
OnboardingStepView ( title : {
VStack ( alignment : . leading ) {
Text ( " Add Sources " )
}
} , hero : {
Image ( systemSymbol : . booksVertical )
. resizable ( )
. aspectRatio ( contentMode : . fit )
. foregroundColor ( . accentColor )
. shadow ( color : . accentColor . opacity ( 0.8 ) , radius : 12 )
} , content : {
VStack ( alignment : . leading , spacing : 16 ) {
Text ( " All apps are provided through sources, which anyone can create and share with the world. " )
Text ( " We have compiled a list of trusted sources for SideStore which you can enable to start sideloading your favorite apps. " )
Text ( " By default, only the source containing SideStore itself is enabled. " )
Toggle ( " Enable Trusted Sources " , isOn : $ areTrustedSourcesEnabled )
}
} , action : {
SwiftUI . Button ( " Continue " ) {
self . setupTrustedSources ( )
}
. buttonStyle ( FilledButtonStyle ( isLoading : self . isLoadingTrustedSources ) )
. disabled ( self . isLoadingTrustedSources )
} )
}
var finishView : some View {
OnboardingStepView ( title : {
VStack ( alignment : . leading ) {
Text ( " Setup Completed " )
}
} , hero : {
Image ( systemSymbol : . checkmark )
. resizable ( )
. aspectRatio ( contentMode : . fit )
. foregroundColor ( . accentColor )
. shadow ( color : . accentColor . opacity ( 0.8 ) , radius : 12 )
} , content : {
VStack ( alignment : . leading , spacing : 16 ) {
Text ( " Congratulations, you did it! 🎉 " )
2023-05-20 22:14:50 +02:00
Text ( " You can now start your sideloading journey. " )
2023-05-19 13:14:15 +02:00
}
} , action : {
SwiftUI . Button ( " Let's Go " ) {
self . finishOnboarding ( )
}
. buttonStyle ( FilledButtonStyle ( ) )
} )
}
2023-05-20 22:14:50 +02:00
@ ViewBuilder
func viewForStep ( _ step : OnboardingStep ) -> some View {
switch step {
case . welcome : self . welcomeStep
case . pairing : self . pairingView
case . wireguard : self . wireguardView
case . wireguardConfig : self . wireguardConfigView
case . addSources : self . addSourcesView
case . finish : self . finishView
}
}
}
extension OnboardingView {
func showNextStep ( ) {
guard self . currentStep != self . enabledSteps . last ,
let index = self . enabledSteps . firstIndex ( of : self . currentStep ) else {
return self . finishOnboarding ( )
}
withAnimation {
self . currentStep = self . enabledSteps [ index + 1 ]
}
}
2023-05-19 13:14:15 +02:00
}
extension OnboardingView {
func importPairingFile ( url : URL ) {
let isSecuredURL = url . startAccessingSecurityScopedResource ( ) = = true
do {
// R e a d t o a s t r i n g
let data = try Data ( contentsOf : url )
let pairing_string = String ( bytes : data , encoding : . utf8 )
if pairing_string = = nil {
// TODO: S h o w e r r o r m e s s a g e
debugPrint ( " Unable to read pairing file " )
// d i s p l a y E r r o r ( " U n a b l e t o r e a d p a i r i n g f i l e " )
}
// S a v e t o a f i l e f o r n e x t l a u n c h
let filename = " ALTPairingFile.mobiledevicepairing "
let fm = FileManager . default
let documentsPath = fm . documentsDirectory . appendingPathComponent ( " / \( filename ) " )
try pairing_string ? . write ( to : documentsPath , atomically : true , encoding : String . Encoding . utf8 )
// S t a r t m i n i m u x e r n o w t h a t w e h a v e a f i l e
start_minimuxer_threads ( pairing_string ! )
// S h o w t h e n e x t o n b o a r d i n g s t e p
self . showNextStep ( )
} catch {
NotificationManager . shared . reportError ( error : error )
}
if ( isSecuredURL ) {
url . stopAccessingSecurityScopedResource ( )
}
}
func start_minimuxer_threads ( _ pairing_file : String ) {
2023-05-20 20:05:36 +02:00
target_minimuxer_address ( )
let documentsDirectory = FileManager . default . documentsDirectory . absoluteString
do {
try start ( pairing_file , documentsDirectory )
} catch {
try ! FileManager . default . removeItem ( at : FileManager . default . documentsDirectory . appendingPathComponent ( " \( pairingFileName ) " ) )
NotificationManager . shared . reportError ( error : error )
debugPrint ( " minimuxer failed to start, please restart SideStore. " , error )
// d i s p l a y E r r o r ( " m i n i m u x e r f a i l e d t o s t a r t , p l e a s e r e s t a r t S i d e S t o r e . \ ( ( e r r o r a s ? L o c a l i z e d E r r o r ) ? . f a i l u r e R e a s o n ? ? " U N K N O W N E R R O R ! ! ! ! ! ! R E P O R T T O G I T H U B I S S U E S ! " ) " )
2023-05-19 13:14:15 +02:00
}
2023-05-20 20:05:36 +02:00
start_auto_mounter ( documentsDirectory )
2023-05-19 13:14:15 +02:00
}
}
extension OnboardingView {
func downloadWireGuardProfile ( ) {
let profileDownloadUrl = " https://github.com/SideStore/SideStore/releases/download/0.3.1/SideStore.conf "
let destinationUrl = FileManager . default . temporaryDirectory . appendingPathComponent ( " SideStore.conf " )
self . isDownloadingWireGuardProfile = true
URLSession . shared . dataTask ( with : URLRequest ( url : URL ( string : profileDownloadUrl ) ! ) ) { data , response , error in
defer { self . isDownloadingWireGuardProfile = false }
if let error {
NotificationManager . shared . reportError ( error : error )
return
}
guard let response = response as ? HTTPURLResponse , 200. . < 300 ~= response . statusCode , let data else {
// TODO: S h o w e r r o r m e s s a g e
return
}
do {
try data . write ( to : destinationUrl )
self . wireGuardProfileFileURL = destinationUrl
} catch {
NotificationManager . shared . reportError ( error : error )
return
}
} . resume ( )
}
func startPingingWireGuardTunnel ( ) {
do {
self . reachabilityNotifier = try Reachability ( hostname : " 10.7.0.1 " )
self . reachabilityNotifier ? . whenReachable = { _ in
self . isWireGuardTunnelReachable = true
}
self . reachabilityNotifier ? . whenUnreachable = { _ in
self . isWireGuardTunnelReachable = false
}
try self . reachabilityNotifier ? . startNotifier ( )
} catch {
// TODO: S h o w e r r o r m e s s a g e
debugPrint ( error )
NotificationManager . shared . reportError ( error : error )
}
}
func stopPingingWireGuardTunnel ( ) {
self . reachabilityNotifier ? . stopNotifier ( )
}
}
extension OnboardingView {
func setupTrustedSources ( ) {
2023-05-20 19:10:51 +02:00
guard self . areTrustedSourcesEnabled else {
return self . showNextStep ( )
}
2023-05-19 13:14:15 +02:00
self . isLoadingTrustedSources = true
AppManager . shared . fetchTrustedSources { result in
switch result {
case . success ( let trustedSources ) :
// C a c h e t r u s t e d s o u r c e I D s .
UserDefaults . shared . trustedSourceIDs = trustedSources . map { $0 . identifier }
// D o n ' t s h o w s o u r c e s w i t h o u t a s o u r c e U R L .
let featuredSourceURLs = trustedSources . compactMap { $0 . sourceURL }
// T h i s c o n t e x t i s n e v e r s a v e d , b u t k e e p s t h e m a n a g e d s o u r c e s a l i v e .
let context = DatabaseManager . shared . persistentContainer . newBackgroundSavingViewContext ( )
let dispatchGroup = DispatchGroup ( )
for sourceURL in featuredSourceURLs {
dispatchGroup . enter ( )
AppManager . shared . fetchSource ( sourceURL : sourceURL , managedObjectContext : context ) { result in
dispatchGroup . leave ( )
}
}
dispatchGroup . notify ( queue : . main ) {
self . isLoadingTrustedSources = false
// S a v e t h e f e t c h e d t r u s t e d s o u r c e s
do {
try context . save ( )
} catch {
NotificationManager . shared . reportError ( error : error )
}
self . showNextStep ( )
}
case . failure ( let error ) :
NotificationManager . shared . reportError ( error : error )
self . isLoadingTrustedSources = false
}
}
}
}
extension OnboardingView {
func finishOnboarding ( ) {
// S e t t h e o n b o a r d i n g c o m p l e t e f l a g
UserDefaults . standard . onboardingComplete = true
if let onDismiss {
onDismiss ( )
} else {
self . dismiss ( )
}
}
}
struct OnboardingView_Previews : PreviewProvider {
static var previews : some View {
2023-05-20 22:14:50 +02:00
ForEach ( OnboardingStep . allCases , id : \ . self ) { step in
2023-05-20 19:10:51 +02:00
Color . red
. ignoresSafeArea ( )
. sheet ( isPresented : . constant ( true ) ) {
2023-05-20 22:14:50 +02:00
OnboardingView ( enabledSteps : [ step ] )
2023-05-20 19:10:51 +02:00
}
}
2023-05-19 13:14:15 +02:00
}
}