feature: added a prompt before installing to customize appId

This commit is contained in:
mahee96
2026-02-02 08:44:49 +05:30
parent d70c916222
commit 30c03aad42
4 changed files with 212 additions and 38 deletions

View File

@@ -8,7 +8,6 @@
import Foundation
import UIKit
import SwiftUI
import UserNotifications
import MobileCoreServices
import Intents
@@ -705,9 +704,36 @@ extension AppManager
}
}
let operation = AppOperation.install(app)
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
var app: AppProtocol = app
// ---- Preflight bundle ID resolution ----
if let presentingViewController {
let originalBundleID = app.bundleIdentifier
let resolution = await self.resolveBundleID(
initial: originalBundleID,
presentingViewController: presentingViewController
)
switch resolution {
case .cancelled:
completionHandler(.failure(OperationError.cancelled))
group.progress.cancel()
case .resolved(let newBundleID):
app = AnyApp(
name: app.name,
bundleIdentifier: newBundleID,
url: app.url,
storeApp: app.storeApp
)
}
}
await self.perform([.install(app)], presentingViewController: presentingViewController, group: group)
}
return group
}
@@ -732,10 +758,11 @@ extension AppManager
}
}
let operation = AppOperation.update(appVersion)
assert(operation.app as AnyObject !== installedApp) // Make sure we never accidentally "update" to already installed app.
assert(appVersion as AnyObject !== installedApp) // Make sure we never accidentally "update" to already installed app.
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
await self.perform([.update(appVersion)], presentingViewController: presentingViewController, group: group)
}
return group.progress
}
@@ -745,16 +772,20 @@ extension AppManager
{
let group = group ?? RefreshGroup()
let operations = installedApps.map { AppOperation.refresh($0) }
return self.perform(operations, presentingViewController: presentingViewController, group: group)
Task{
await self.perform(installedApps.map { .refresh($0) }, presentingViewController: presentingViewController, group: group)
}
return group
}
func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
{
let group = RefreshGroup()
let operation = AppOperation.activate(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
await self.perform([.activate(installedApp)], presentingViewController: presentingViewController, group: group)
}
group.completionHandler = { (results) in
do
@@ -812,8 +843,9 @@ extension AppManager
}
}
let operation = AppOperation.deactivate(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
await self.perform([.deactivate(installedApp)], presentingViewController: presentingViewController, group: group)
}
}
}
@@ -837,8 +869,9 @@ extension AppManager
}
}
let operation = AppOperation.backup(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
await self.perform([.backup(installedApp)], presentingViewController: presentingViewController, group: group)
}
}
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
@@ -863,8 +896,9 @@ extension AppManager
}
}
let operation = AppOperation.restore(installedApp)
self.perform([operation], presentingViewController: presentingViewController, group: group)
Task{
await self.perform([.restore(installedApp)], presentingViewController: presentingViewController, group: group)
}
}
func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
@@ -1091,7 +1125,7 @@ private extension AppManager
}
@discardableResult
private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup
private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) async -> RefreshGroup
{
let operations = operations.filter { self.progress(for: $0) == nil || self.progress(for: $0)?.isCancelled == true }
@@ -1226,7 +1260,7 @@ private extension AppManager
{
let progress = Progress.discreteProgress(totalUnitCount: 100)
let context = context ?? InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
assert(context.authenticatedContext === group.context)
context.beginInstallationHandler = { (installedApp) in
@@ -1304,7 +1338,7 @@ private extension AppManager
/* Verify App */
let permissionsMode = UserDefaults.shared.permissionCheckingDisabled ? .none : permissionReviewMode
let verifyOperation = VerifyAppOperation(permissionsMode: permissionsMode, context: context)
let verifyOperation = VerifyAppOperation(permissionsMode: permissionsMode, context: context, customBundleId: app.bundleIdentifier)
verifyOperation.resultHandler = { (result) in
do
{
@@ -1457,7 +1491,7 @@ private extension AppManager
let patchAppURL = URL(string: patchAppLink)
else { throw OperationError.invalidApp }
let patchApp = AnyApp(name: app.name, bundleIdentifier: app.bundleIdentifier, url: patchAppURL, storeApp: nil)
let patchApp = AnyApp(name: app.name, bundleIdentifier: context.bundleIdentifier, url: patchAppURL, storeApp: nil)
DispatchQueue.main.async {
let storyboard = UIStoryboard(name: "PatchApp", bundle: nil)
@@ -1479,7 +1513,7 @@ private extension AppManager
presentingViewController?.dismiss(animated: true, completion: nil)
}
}
presentingViewController.present(navigationController, animated: true, completion: nil)
presentingViewController.present(navigationController, animated: true, completion: nil)
}
}
catch
@@ -2274,3 +2308,123 @@ private extension AppManager
}
}
}
private enum BundleIDAlertKeys {
static var okAction: UInt8 = 0
}
private func _isValidBundleID(_ value: String) -> Bool {
let pattern = #"^[A-Za-z][A-Za-z0-9\-]*(\.[A-Za-z0-9\-]+)+$"#
return value.range(of: pattern, options: .regularExpression) != nil
}
private extension UIResponder {
@objc func _validateBundleIDText(_ sender: UITextField) {
let isValid = sender.text.map(_isValidBundleID) ?? false
sender.backgroundColor =
isValid || sender.text?.isEmpty == true
? .clear
: UIColor.systemRed.withAlphaComponent(0.2)
if
let alert = sender.superview?.superview as? UIAlertController,
let okAction = objc_getAssociatedObject(alert, &BundleIDAlertKeys.okAction) as? UIAlertAction
{
okAction.isEnabled = isValid
}
}
}
private extension AppManager {
func _presentBundleIDOverrideDialog(
bundleIdentifier: String,
presentingViewController: UIViewController,
completion: @escaping (BundleIDResolution) -> Void
) {
let alert = self._makeBundleIDOverrideAlert(
initialBundleID: bundleIdentifier,
completion: completion
)
presentingViewController.present(alert, animated: true)
}
func _makeBundleIDOverrideAlert(
initialBundleID: String,
completion: @escaping (BundleIDResolution) -> Void
) -> UIAlertController {
let alert = UIAlertController(
title: NSLocalizedString("Override Bundle Identifier", comment: ""),
message: nil,
preferredStyle: .alert
)
var okAction: UIAlertAction!
alert.addTextField { textField in
textField.text = initialBundleID
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.addTarget(
nil,
action: #selector(UIResponder._validateBundleIDText(_:)),
for: .editingChanged
)
}
okAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default) { _ in
completion(.resolved(alert.textFields?.first?.text ?? initialBundleID))
}
okAction.isEnabled = _isValidBundleID(initialBundleID)
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in
completion(.cancelled)
}
alert.addAction(cancelAction)
alert.addAction(okAction)
objc_setAssociatedObject(
alert,
&BundleIDAlertKeys.okAction,
okAction,
.OBJC_ASSOCIATION_ASSIGN
)
return alert
}
}
// ---- Part 1: Add async resolver ----
private extension AppManager {
enum BundleIDResolution {
case resolved(String)
case cancelled
}
@MainActor
func resolveBundleID(
initial: String,
presentingViewController: UIViewController
) async -> BundleIDResolution {
await withCheckedContinuation { continuation in
let alert = self._makeBundleIDOverrideAlert(
initialBundleID: initial
) { result in
continuation.resume(returning: result)
}
presentingViewController.present(alert, animated: true)
}
}
}

View File

@@ -54,7 +54,8 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
Logger.sideload.notice("Fetching provisioning profiles for app \(self.context.bundleIdentifier, privacy: .public)...")
self.progress.totalUnitCount = Int64(1 + app.appExtensions.count)
let effectiveBundleId = self.context.bundleIdentifier
self.prepareProvisioningProfile(for: app, parentApp: nil, team: team, session: session) { (result) in
do
{
@@ -62,7 +63,7 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
let profile = try result.get()
var profiles = [app.bundleIdentifier: profile]
var profiles = [effectiveBundleId: profile]
var error: Error?
let dispatchGroup = DispatchGroup()
@@ -220,19 +221,30 @@ extension FetchProvisioningProfilesOperation
// Or, if the app _is_ installed but with a different team, we need to create a new
// bundle identifier anyway to prevent collisions with the previous team.
let parentBundleID = parentApp?.bundleIdentifier ?? app.bundleIdentifier
let effectiveParentBundleID = self.context.bundleIdentifier
let updatedParentBundleID: String
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore (and its extensions).
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
updatedParentBundleID = effectiveParentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
else
{
updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
updatedParentBundleID = effectiveParentBundleID + "." + team.identifier // Append just team identifier to make it harder to track.
}
if let parentApp = parentApp,
app.bundleIdentifier.hasPrefix(parentBundleID + ".")
{
let suffix = String(app.bundleIdentifier.dropFirst(parentBundleID.count))
bundleID = updatedParentBundleID + suffix
}
else
{
bundleID = updatedParentBundleID
}
bundleID = app.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID)
}
let preferredName: String

View File

@@ -55,6 +55,7 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
let prepareAppProgress = Progress.discreteProgress(totalUnitCount: 2)
self.progress.addChild(prepareAppProgress, withPendingUnitCount: 3)
let effectiveBundleId = self.context.bundleIdentifier
let prepareAppBundleProgress = self.prepareAppBundle(for: app, profiles: profiles, appexBundleIds: context.appexBundleIds ?? [:]) { (result) in
guard let appBundleURL = self.process(result) else { return }
@@ -65,7 +66,13 @@ final class ResignAppOperation: ResultOperation<ALTApplication>
// Finish
do
{
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
let updatedApp = AnyApp(
name: app.name,
bundleIdentifier: effectiveBundleId,
url: app.fileURL,
storeApp: app.storeApp
)
let destinationURL = InstalledApp.refreshedIPAURL(for: updatedApp)
try FileManager.default.copyItem(at: resignedURL, to: destinationURL, shouldReplace: true)
print("Successfully resigned app to \(destinationURL.absoluteString)")
@@ -110,15 +117,13 @@ private extension ResignAppOperation
func prepareAppBundle(for app: ALTApplication, profiles: [String: ALTProvisioningProfile], appexBundleIds: [String: String], completionHandler: @escaping (Result<URL, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 1)
let bundleIdentifier = app.bundleIdentifier
let openURL = InstalledApp.openAppURL(for: app)
let fileURL = app.fileURL
let identifier = context.bundleIdentifier
func prepare(_ bundle: Bundle, additionalInfoDictionaryValues: [String: Any] = [:]) throws
{
guard let identifier = bundle.bundleIdentifier else { throw ALTError(.missingAppBundle) }
guard let profile = context.useMainProfile ? profiles.values.first : profiles[identifier] else { throw ALTError(.missingProvisioningProfile) }
guard var infoDictionary = bundle.completeInfoDictionary else { throw ALTError(.missingInfoPlist) }
@@ -189,7 +194,7 @@ private extension ResignAppOperation
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
"CFBundleURLName": bundleIdentifier,
"CFBundleURLName": identifier,
"CFBundleURLSchemes": [openURL.scheme!]] as [String : Any]
allURLSchemes.append(altstoreURLScheme)
@@ -198,7 +203,7 @@ private extension ResignAppOperation
if app.isAltStoreApp
{
guard let udid = fetch_udid()?.toString() as? String else { throw OperationError.unknownUDID }
guard let pairingFileString = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) as? String else { throw OperationError.unknownUDID }
guard Bundle.main.object(forInfoDictionaryKey: Bundle.Info.devicePairingString) is String else { throw OperationError.unknownUDID }
additionalValues[Bundle.Info.devicePairingString] = "<insert pairing file here>"
additionalValues[Bundle.Info.deviceID] = udid
additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID

View File

@@ -38,11 +38,13 @@ final class VerifyAppOperation: ResultOperation<Void>
{
let permissionsMode: PermissionReviewMode
let context: InstallAppOperationContext
var customBundleId: String?
init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext)
init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext, customBundleId: String? = nil)
{
self.permissionsMode = permissionsMode
self.context = context
self.customBundleId = customBundleId
super.init()
}
@@ -65,7 +67,8 @@ final class VerifyAppOperation: ResultOperation<Void>
}
if !["ny.litritt.ignited", "com.litritt.ignited"].contains(where: { $0 == app.bundleIdentifier }) {
guard app.bundleIdentifier == self.context.bundleIdentifier else {
let bundleId = customBundleId ?? app.bundleIdentifier
guard bundleId == self.context.bundleIdentifier else {
throw VerificationError.mismatchedBundleIdentifiers(sourceBundleID: self.context.bundleIdentifier, app: app)
}
}