Add onboarding screens for an easy setup of SideStore

This commit is contained in:
Fabian Thies
2023-05-19 13:14:15 +02:00
parent 89609ad35c
commit 653d80b88e
9 changed files with 849 additions and 1 deletions

View File

@@ -63,6 +63,10 @@
1F943C6F2927F90400ABE095 /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5B82927E0EE00B8D837 /* NewsView.swift */; };
1F943C702927F90400ABE095 /* BrowseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BA2927E0F800B8D837 /* BrowseView.swift */; };
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAFC5BD2927E10D00B8D837 /* MyAppsView.swift */; };
1F981B1129AA0FAE0014950E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1029AA0FAE0014950E /* OnboardingView.swift */; };
1F981B1329AA101F0014950E /* OnboardingStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1229AA101F0014950E /* OnboardingStepView.swift */; };
1F981B1529AA1E070014950E /* AppIconsShowcase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1429AA1E070014950E /* AppIconsShowcase.swift */; };
1F981B1729AA34A70014950E /* AppStoreProductView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F981B1629AA34A70014950E /* AppStoreProductView.swift */; };
1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA1C8C9294906890083119D /* MyAppsViewModel.swift */; };
1FA5A6CA298E8B2F007BA946 /* RefreshAttemptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */; };
1FA5A6CC298E8FE4007BA946 /* MailComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */; };
@@ -76,6 +80,8 @@
1FB96FCF292BBBCA007E68D1 /* SiriShortcutSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */; };
1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FEB292C171D007E68D1 /* NotificationManager.swift */; };
1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */; };
1FF8C6182A1780C60041352C /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF8C6172A1780C60041352C /* ActivityView.swift */; };
1FF8C61B2A1782F10041352C /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 1FF8C61A2A1782F10041352C /* Reachability */; };
1FFA56C2299994390011B6F5 /* OutputCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFA56C1299994390011B6F5 /* OutputCapturer.swift */; };
1FFA56C52999978C0011B6F5 /* LocalConsole in Frameworks */ = {isa = PBXBuildFile; productRef = 1FFA56C42999978C0011B6F5 /* LocalConsole */; };
1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */; };
@@ -609,6 +615,10 @@
1F943C632927EF4200ABE095 /* NewsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItemView.swift; sourceTree = "<group>"; };
1F943C652927F36600ABE095 /* NewsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewModel.swift; sourceTree = "<group>"; };
1F943C672927F39400ABE095 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
1F981B1029AA0FAE0014950E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
1F981B1229AA101F0014950E /* OnboardingStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStepView.swift; sourceTree = "<group>"; };
1F981B1429AA1E070014950E /* AppIconsShowcase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconsShowcase.swift; sourceTree = "<group>"; };
1F981B1629AA34A70014950E /* AppStoreProductView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreProductView.swift; sourceTree = "<group>"; };
1FA1C8C9294906890083119D /* MyAppsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewModel.swift; sourceTree = "<group>"; };
1FA5A6C9298E8B2F007BA946 /* RefreshAttemptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsView.swift; sourceTree = "<group>"; };
1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposeView.swift; sourceTree = "<group>"; };
@@ -629,6 +639,7 @@
1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriShortcutSetupView.swift; sourceTree = "<group>"; };
1FB96FEB292C171D007E68D1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonProgressViewStyle.swift; sourceTree = "<group>"; };
1FF8C6172A1780C60041352C /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
1FFA56C1299994390011B6F5 /* OutputCapturer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputCapturer.swift; sourceTree = "<group>"; };
1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = "<group>"; };
9961EC2D29BE9F2E00AF2C6F /* minimuxer-helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "minimuxer-helpers.swift"; path = "Dependencies/minimuxer/minimuxer-helpers.swift"; sourceTree = SOURCE_ROOT; };
@@ -1046,6 +1057,7 @@
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */,
9922FFEC29B501C50020F868 /* Starscream in Frameworks */,
1FF8C61B2A1782F10041352C /* Reachability in Frameworks */,
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */,
B3C395F4284F35DD00DA9E2F /* Nuke in Frameworks */,
@@ -1126,15 +1138,28 @@
1F6284D6295218980060AAD8 /* DocumentPicker.swift */,
1F545E84298D84CF00589F68 /* FilePreviewView.swift */,
1FA5A6CB298E8FE4007BA946 /* MailComposeView.swift */,
1F981B1629AA34A70014950E /* AppStoreProductView.swift */,
1FF8C6172A1780C60041352C /* ActivityView.swift */,
);
path = "UIView Representables";
sourceTree = "<group>";
};
1F981B0F29AA0F9B0014950E /* Onboarding */ = {
isa = PBXGroup;
children = (
1F981B1029AA0FAE0014950E /* OnboardingView.swift */,
1F981B1229AA101F0014950E /* OnboardingStepView.swift */,
1F981B1429AA1E070014950E /* AppIconsShowcase.swift */,
);
path = Onboarding;
sourceTree = "<group>";
};
1FAFC5B02927E01400B8D837 /* Views */ = {
isa = PBXGroup;
children = (
1FAFC5B52927E06300B8D837 /* RootView.swift */,
1FAFC5B12927E02E00B8D837 /* Authentication */,
1F981B0F29AA0F9B0014950E /* Onboarding */,
1FAFC5B22927E03300B8D837 /* News */,
1FAFC5B32927E03D00B8D837 /* Browse */,
1FAFC5BC2927E0FD00B8D837 /* My Apps */,
@@ -2312,6 +2337,7 @@
1F07F5662955D16A00F7BE95 /* SFSafeSymbols */,
1F1295802989B51F0048FCB9 /* ExpandableText */,
1FFA56C42999978C0011B6F5 /* LocalConsole */,
1FF8C61A2A1782F10041352C /* Reachability */,
);
productName = AltStore;
productReference = BFD2476A2284B9A500981D42 /* SideStore.app */;
@@ -2389,6 +2415,7 @@
1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */,
1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */,
1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */,
);
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
projectDirPath = "";
@@ -2814,6 +2841,7 @@
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */,
1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */,
1F981B1529AA1E070014950E /* AppIconsShowcase.swift in Sources */,
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */,
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
@@ -2841,10 +2869,13 @@
1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */,
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
1F07F56B2955F11500F7BE95 /* AppScreenshotsPreview.swift in Sources */,
1F981B1329AA101F0014950E /* OnboardingStepView.swift in Sources */,
BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */,
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
1F981B1729AA34A70014950E /* AppStoreProductView.swift in Sources */,
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
B3EE16B62925E27D00B3B1F5 /* AnisetteManager.swift in Sources */,
1FF8C6182A1780C60041352C /* ActivityView.swift in Sources */,
1F943C6E2927F90400ABE095 /* NewsItemView.swift in Sources */,
BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */,
B39F16152918D7DA002E9404 /* Consts+Proxy.swift in Sources */,
@@ -2917,6 +2948,7 @@
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */,
1F943C702927F90400ABE095 /* BrowseView.swift in Sources */,
1F981B1129AA0FAE0014950E /* OnboardingView.swift in Sources */,
1F943C692927F8F200ABE095 /* RootView.swift in Sources */,
1FB84BA62928DE08006A5CF4 /* AppDetailView.swift in Sources */,
D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */,
@@ -3772,6 +3804,14 @@
kind = branch;
};
};
1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
requirement = {
branch = master;
kind = branch;
};
};
1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/duraidabdul/LocalConsole.git";
@@ -3893,6 +3933,11 @@
package = 1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */;
productName = AsyncImage;
};
1FF8C61A2A1782F10041352C /* Reachability */ = {
isa = XCSwiftPackageProductDependency;
package = 1FF8C6192A1782F10041352C /* XCRemoteSwiftPackageReference "Reachability" */;
productName = Reachability;
};
1FFA56C42999978C0011B6F5 /* LocalConsole */ = {
isa = XCSwiftPackageProductDependency;
package = 1FFA56C32999978C0011B6F5 /* XCRemoteSwiftPackageReference "LocalConsole" */;

View File

@@ -90,6 +90,15 @@
"version" : "1.10.1"
}
},
{
"identity" : "reachability.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ashleymills/Reachability.swift",
"state" : {
"branch" : "master",
"revision" : "a81b7367f2c46875f29577e03a60c39cdfad0c8d"
}
},
{
"identity" : "semanticversion",
"kind" : "remoteSourceControl",

View File

@@ -54,15 +54,29 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
#if !targetEnvironment(simulator)
if !UserDefaults.standard.onboardingComplete {
self.showOnboarding()
return
}
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
guard let pf = fetchPairingFile() else {
displayError("Device pairing file not found.")
self.showOnboarding(step: .pairing)
return
}
start_minimuxer_threads(pf)
#endif
}
func showOnboarding(step: OnboardingView.OnboardingStep = .welcome) {
let onboardingView = OnboardingView(onDismiss: { self.dismiss(animated: true) }, currentStep: step)
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
let navigationController = UINavigationController(rootViewController: UIHostingController(rootView: onboardingView))
navigationController.isNavigationBarHidden = true
navigationController.isModalInPresentation = true
self.present(navigationController, animated: true)
}
func fetchPairingFile() -> String? {
let filename = "ALTPairingFile.mobiledevicepairing"

View File

@@ -0,0 +1,21 @@
//
// ActivityView.swift
// SideStore
//
// Created by Fabian Thies on 19.05.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import UIKit
struct ActivityView: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {}
}

View File

@@ -0,0 +1,87 @@
//
// AppStoreProductView.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import StoreKit
struct AppStoreView: UIViewControllerRepresentable {
typealias UIViewControllerType = AppStoreProductViewController
var isVisible: Binding<Bool>
let itunesItemId: Int
func makeUIViewController(context: Context) -> AppStoreProductViewController {
AppStoreProductViewController(isVisible: self.isVisible, itunesId: self.itunesItemId)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
if self.isVisible.wrappedValue {
uiViewController.presentStoreProduct()
}
}
}
class AppStoreProductViewController: UIViewController {
private var isVisible: Binding<Bool>
private let itunesId: Int
init(isVisible: Binding<Bool>, itunesId: Int) {
self.isVisible = isVisible
self.itunesId = itunesId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
func presentStoreProduct() {
let storeProductViewController = SKStoreProductViewController()
storeProductViewController.delegate = self
let parameters = [SKStoreProductParameterITunesItemIdentifier: self.itunesId]
storeProductViewController.loadProduct(withParameters: parameters) { (success, error) -> Void in
if let error = error {
print("Failed to load App Store product: \(error.localizedDescription)")
}
guard success else {
return
}
self.present(storeProductViewController, animated: true, completion: nil)
}
}
}
// MARK: - SKStoreProductViewControllerDelegate
extension AppStoreProductViewController: SKStoreProductViewControllerDelegate {
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
DispatchQueue.main.async {
self.isVisible.wrappedValue = false
}
// viewController.presentingViewController?.dismiss(animated: true, completion: nil)
}
}

View File

@@ -0,0 +1,89 @@
//
// AppIconsShowcase.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
struct AppIconsShowcase: View {
@State var animationProgress = 0.0
@State var animation2Progress = 0.0
var body: some View {
VStack {
GeometryReader { proxy in
ZStack(alignment: .bottom) {
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.2 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: -0.3*proxy.size.width * self.animationProgress, y: -30)
.rotationEffect(.degrees(-20 * self.animationProgress))
.shadow(radius: 8 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.25 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: -0.15*proxy.size.width * self.animationProgress, y: -10)
.rotationEffect(.degrees(-10 * self.animationProgress))
.shadow(radius: 12 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.2 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: self.animationProgress*0.3*proxy.size.width, y: -30)
.rotationEffect(.degrees(self.animationProgress*20))
.shadow(radius: 8 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.25 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.offset(x: self.animationProgress * 0.15*proxy.size.width, y: -10)
.rotationEffect(.degrees(self.animationProgress * 10))
.shadow(radius: 12 * self.animationProgress)
Image(uiImage: UIImage(named: "AppIcon")!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 0.3 * proxy.size.width)
.clipShape(RoundedRectangle(cornerRadius: 24, style: .circular))
.shadow(radius: 16 * self.animationProgress + 8 * self.animation2Progress)
.scaleEffect(1.0 + 0.05 * self.animation2Progress)
}
.frame(maxWidth: proxy.size.width)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.spring()) {
self.animationProgress = 1.0
self.animation2Progress = 1.0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
withAnimation(.spring()) {
self.animation2Progress = 0.0
}
}
}
}
}
}
struct AppIconsShowcase_Previews: PreviewProvider {
static var previews: some View {
AppIconsShowcase()
.frame(height: 150)
}
}

View File

@@ -0,0 +1,137 @@
//
// OnboardingStepView.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
struct OnboardingStep<Title: View, Hero: View, Content: View, Action: View> {
@ViewBuilder
var title: Title
@ViewBuilder
var hero: Hero
@ViewBuilder
var content: Content
@ViewBuilder
var action: Action
}
struct OnboardingStepView<Title: View, Hero: View, Content: View, Action: View>: View {
@ViewBuilder
var title: Title
@ViewBuilder
var hero: Hero
@ViewBuilder
var content: Content
@ViewBuilder
var action: Action
var body: some View {
VStack(spacing: 64) {
self.title
.font(.largeTitle.weight(.bold))
.frame(maxWidth: .infinity, alignment: .leading)
self.hero
.frame(height: 150)
self.content
Spacer()
self.action
}
.frame(maxWidth: .infinity)
.padding()
}
}
struct OnboardingStepView_Previews: PreviewProvider {
@State
static var isWireGuardAppStorePageVisible = false
static var previews: some View {
OnboardingStepView(title: {
VStack(alignment: .leading) {
Text("Welcome to")
Text("SideStore")
.foregroundColor(.accentColor)
}
}, hero: {
AppIconsShowcase()
}, content: {
VStack(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") {
}
.buttonStyle(FilledButtonStyle())
})
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 {
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.")
}
}, action: {
SwiftUI.Button("Continue") {
}
.buttonStyle(FilledButtonStyle())
})
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 {
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.")
}
}, action: {
SwiftUI.Button("Show in App Store") {
}
.buttonStyle(FilledButtonStyle())
AppStoreView(isVisible: self.$isWireGuardAppStorePageVisible, itunesItemId: 1441195209)
.frame(width: .zero, height: .zero)
})
}
}

View File

@@ -0,0 +1,444 @@
//
// OnboardingView.swift
// SideStore
//
// Created by Fabian Thies on 25.02.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import CoreData
import AltStoreCore
import minimuxer
import Reachability
import UniformTypeIdentifiers
struct OnboardingView: View {
enum OnboardingStep: Int {
case welcome, pairing, wireguard, wireguardConfig, addSources, finish
}
@Environment(\.dismiss) var dismiss
// Temporary workaround for UIKit compatibility
var onDismiss: (() -> Void)? = nil
@State var currentStep: OnboardingStep = .wireguard //.welcome
@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) {
welcomeStep
.tag(OnboardingStep.welcome)
.highPriorityGesture(DragGesture())
pairingView
.tag(OnboardingStep.pairing)
.highPriorityGesture(DragGesture())
wireguardView
.tag(OnboardingStep.wireguard)
.highPriorityGesture(DragGesture())
wireguardConfigView
.tag(OnboardingStep.wireguardConfig)
.highPriorityGesture(DragGesture())
addSourcesView
.tag(OnboardingStep.addSources)
.highPriorityGesture(DragGesture())
finishView
.tag(OnboardingStep.finish)
.highPriorityGesture(DragGesture())
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.bottom)
.background(Color.accentColor.opacity(0.1).edgesIgnoringSafeArea(.all))
.onChange(of: self.currentStep) { step in
switch step {
case .wireguardConfig:
self.startPingingWireGuardTunnel()
default:
self.stopPingingWireGuardTunnel()
}
}
}
func showNextStep() {
withAnimation {
self.currentStep = OnboardingStep(rawValue: self.currentStep.rawValue + 1) ?? self.currentStep
}
}
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) {
Text("SideStore supports sideloading even on non-jailbroken devices.")
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! 🎉")
Text("You can start your sideloading journey.")
}
}, action: {
SwiftUI.Button("Let's Go") {
self.finishOnboarding()
}
.buttonStyle(FilledButtonStyle())
})
}
}
extension OnboardingView {
func importPairingFile(url: URL) {
let isSecuredURL = url.startAccessingSecurityScopedResource() == true
do {
// Read to a string
let data = try Data(contentsOf: url)
let pairing_string = String(bytes: data, encoding: .utf8)
if pairing_string == nil {
// TODO: Show error message
debugPrint("Unable to read pairing file")
// displayError("Unable to read pairing file")
}
// Save to a file for next launch
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)
// Start minimuxer now that we have a file
start_minimuxer_threads(pairing_string!)
// Show the next onboarding step
self.showNextStep()
} catch {
NotificationManager.shared.reportError(error: error)
}
if (isSecuredURL) {
url.stopAccessingSecurityScopedResource()
}
}
func start_minimuxer_threads(_ pairing_file: String) {
set_usbmuxd_socket()
#if false // Retries
var res = start_minimuxer(pairing_file: pairing_file)
var attempts = 10
while (attempts != 0 && res != 0) {
print("start_minimuxer `res` != 0, retry #\(attempts)")
res = start_minimuxer(pairing_file: pairing_file)
attempts -= 1
}
#else
let res = start_minimuxer(pairing_file: pairing_file)
#endif
if res != 0 {
// TODO: Show error message
debugPrint("minimuxer failed to start. Incorrect arguments were passed.")
// displayError("minimuxer failed to start. Incorrect arguments were passed.")
}
auto_mount_dev_image()
}
}
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: Show error message
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: Show error message
debugPrint(error)
NotificationManager.shared.reportError(error: error)
}
}
func stopPingingWireGuardTunnel() {
self.reachabilityNotifier?.stopNotifier()
}
}
extension OnboardingView {
func setupTrustedSources() {
self.isLoadingTrustedSources = true
AppManager.shared.fetchTrustedSources { result in
switch result {
case .success(let trustedSources):
// Cache trusted source IDs.
UserDefaults.shared.trustedSourceIDs = trustedSources.map { $0.identifier }
// Don't show sources without a sourceURL.
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
// This context is never saved, but keeps the managed sources alive.
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
// Save the fetched trusted sources
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() {
// Set the onboarding complete flag
UserDefaults.standard.onboardingComplete = true
if let onDismiss {
onDismiss()
} else {
self.dismiss()
}
}
}
struct OnboardingView_Previews: PreviewProvider {
static var previews: some View {
Color.red
.ignoresSafeArea()
.sheet(isPresented: .constant(true)) {
OnboardingView()
}
}
}

View File

@@ -20,6 +20,7 @@ public extension UserDefaults
}()
@NSManaged var firstLaunch: Date?
@NSManaged var onboardingComplete: Bool
@NSManaged var requiresAppGroupMigration: Bool
@NSManaged var textServer: Bool
@NSManaged var textInputAnisetteURL: String?
@@ -71,6 +72,7 @@ public extension UserDefaults
let localServerSupportsRefreshing = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14)
let defaults = [
#keyPath(UserDefaults.onboardingComplete): false,
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions,