diff --git a/Sources/EeveeSpotify/BlockStoreReviews.x.swift b/Sources/EeveeSpotify/BlockStoreReviews.x.swift new file mode 100644 index 00000000..c216dd35 --- /dev/null +++ b/Sources/EeveeSpotify/BlockStoreReviews.x.swift @@ -0,0 +1,12 @@ +import Orion +import UIKit +import StoreKit + +// uncomment if you're building for jailbreak, cause ipa crashes: +// Fatal error: Error in tweak EeveeSpotify: Failed to hook method -[SKStoreReviewController requestReview:] (Could not hook method) +// Fatal error: Error in tweak EeveeSpotify: Failed to hook method -[SKStoreReviewController requestReviewInScene:] (Could not hook method) + +// class SKStoreReviewControllerHook: ClassHook { +// func requestReview() { } +// func requestReviewInScene(_ scene: UIWindowScene) { } +// } diff --git a/Sources/EeveeSpotify/Helpers/OfflineHelper.swift b/Sources/EeveeSpotify/Helpers/OfflineHelper.swift new file mode 100755 index 00000000..1c5213b2 --- /dev/null +++ b/Sources/EeveeSpotify/Helpers/OfflineHelper.swift @@ -0,0 +1,63 @@ +import Foundation + +class OfflineHelper { + + static let persistentCachePath = FileManager.default.urls( + for: .applicationSupportDirectory, in: .userDomainMask + ) + .first! + .appendingPathComponent("PersistentCache") + + // + + static var offlineBnkPath: URL { + persistentCachePath.appendingPathComponent("offline.bnk") + } + + static var eeveeBnkPath: URL { + persistentCachePath.appendingPathComponent("eevee.bnk") + } + + static var offlineBnkData: Data { + get throws { try Data(contentsOf: offlineBnkPath) } + } + + static var eeveeBnkData: Data { + get throws { try Data(contentsOf: eeveeBnkPath) } + } + + // + + private static func writeOfflineBnkData(_ data: Data) throws { + try data.write(to: offlineBnkPath) + } + + private static func writeEeveeBnkData(_ data: Data) throws { + try data.write(to: eeveeBnkPath) + } + + // + + static func restoreFromEeveeBnk() throws { + try writeOfflineBnkData(try eeveeBnkData) + } + + static func backupToEeveeBnk() throws { + try writeEeveeBnkData(try offlineBnkData) + } + + static func patchOfflineBnk() throws { + + let fileData = try offlineBnkData + + let usernameLength = Int(fileData[8]) + let username = Data(fileData[9..<9 + usernameLength]) + + var blankData = try BundleHelper.shared.premiumBlankData() + + blankData.insert(UInt8(usernameLength), at: 8) + blankData.insert(contentsOf: username, at: 9) + + try writeOfflineBnkData(blankData) + } +} diff --git a/Sources/EeveeSpotify/Helpers/PopUpHelper.swift b/Sources/EeveeSpotify/Helpers/PopUpHelper.swift index 361fb841..10b2e8d7 100644 --- a/Sources/EeveeSpotify/Helpers/PopUpHelper.swift +++ b/Sources/EeveeSpotify/Helpers/PopUpHelper.swift @@ -13,35 +13,50 @@ class PopUpHelper { .shared() static func showPopUp( + delayed: Bool = false, message: String, - buttonText: String + buttonText: String, + secondButtonText: String? = nil, + onPrimaryClick: (() -> Void)? = nil, + onSecondaryClick: (() -> Void)? = nil ) { - if isPopUpShowing { - return - } + DispatchQueue.main.asyncAfter(deadline: delayed ? .now() + 3.0 : .now()) { + + if isPopUpShowing { + return + } - let model = Dynamic.SPTEncorePopUpDialogModel - .alloc(interface: SPTEncorePopUpDialogModel.self) - .initWithTitle( - "EeveeSpotify", - description: message, - image: nil, - primaryButtonTitle: buttonText, - secondaryButtonTitle: nil - ) - - let dialog = Dynamic.SPTEncorePopUpDialog - .alloc(interface: SPTEncorePopUpDialog.self) - .`init`() - - dialog.update(model) - dialog.setEventHandler({ - sharedPresenter.dismissPopupWithAnimate(true, clearQueue: false, completion: nil) - isPopUpShowing.toggle() - }) + let model = Dynamic.SPTEncorePopUpDialogModel + .alloc(interface: SPTEncorePopUpDialogModel.self) + .initWithTitle( + "EeveeSpotify", + description: message, + image: nil, + primaryButtonTitle: buttonText, + secondaryButtonTitle: secondButtonText + ) + + let dialog = Dynamic.SPTEncorePopUpDialog + .alloc(interface: SPTEncorePopUpDialog.self) + .`init`() + + dialog.update(model) + dialog.setEventHandler({ state in + + switch (state) { + + case .primary: onPrimaryClick?() + case .secondary: onSecondaryClick?() + + } - sharedPresenter.presentPopUp(dialog) - isPopUpShowing.toggle() + sharedPresenter.dismissPopupWithAnimate(true, clearQueue: false, completion: nil) + isPopUpShowing.toggle() + }) + + isPopUpShowing.toggle() + sharedPresenter.presentPopUp(dialog) + } } } diff --git a/Sources/EeveeSpotify/HookedInstances.x.swift b/Sources/EeveeSpotify/HookedInstances.x.swift new file mode 100644 index 00000000..52380042 --- /dev/null +++ b/Sources/EeveeSpotify/HookedInstances.x.swift @@ -0,0 +1,19 @@ +import Orion + +class HookedInstances { + static var productState: SPTCoreProductState? +} + +class SPTCoreProductStateInstanceHook: ClassHook { + + static let targetName = "SPTCoreProductState" + + func stringForKey(_ key: String) -> String { + + HookedInstances.productState = Dynamic.convert( + target, + to: SPTCoreProductState.self + ) + return orig.stringForKey(key) + } +} diff --git a/Sources/EeveeSpotify/Models/SPTCoreProductState.swift b/Sources/EeveeSpotify/Models/SPTCoreProductState.swift new file mode 100644 index 00000000..92716379 --- /dev/null +++ b/Sources/EeveeSpotify/Models/SPTCoreProductState.swift @@ -0,0 +1,5 @@ +import Foundation + +@objc protocol SPTCoreProductState { + func stringForKey(_ key: String) -> String +} \ No newline at end of file diff --git a/Sources/EeveeSpotify/Models/SPTEncorePopUpDialog.swift b/Sources/EeveeSpotify/Models/SPTEncorePopUpDialog.swift index 24a1f855..ce585795 100644 --- a/Sources/EeveeSpotify/Models/SPTEncorePopUpDialog.swift +++ b/Sources/EeveeSpotify/Models/SPTEncorePopUpDialog.swift @@ -3,5 +3,10 @@ import Foundation @objc protocol SPTEncorePopUpDialog { func `init`() -> SPTEncorePopUpDialog func update(_ popUpModel: SPTEncorePopUpDialogModel) - func setEventHandler(_ handler: @escaping () -> Void) + func setEventHandler(_ handler: @escaping (_ state: ClickState) -> Void) +} + +@objc enum ClickState: Int { + case primary + case secondary } \ No newline at end of file diff --git a/Sources/EeveeSpotify/OfflineObserver.swift b/Sources/EeveeSpotify/OfflineObserver.swift new file mode 100644 index 00000000..9a6cf383 --- /dev/null +++ b/Sources/EeveeSpotify/OfflineObserver.swift @@ -0,0 +1,52 @@ +import Foundation +import UIKit + +func exitApplication() { + + UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) + Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in + exit(EXIT_SUCCESS) + } +} + +class OfflineObserver: NSObject, NSFilePresenter { + + var presentedItemURL: URL? + var presentedItemOperationQueue: OperationQueue + + override init() { + presentedItemURL = OfflineHelper.offlineBnkPath + presentedItemOperationQueue = .main + } + + func presentedItemDidChange() { + + let productState = HookedInstances.productState! + + if productState.stringForKey("player-license") == "premium" { + + do { + try OfflineHelper.backupToEeveeBnk() + NSLog("[EeveeSpotify] Settings has changed, updated eevee.bnk") + } + catch { + NSLog("[EeveeSpotify] Unable to update eevee.bnk: \(error)") + } + + return + } + + PopUpHelper.showPopUp( + message: "Spotify has just reloaded user data, and you've been switched to the Free plan. It's fine; simply restart the app, and the tweak will patch the data again. If this doesn't work, there might be a problem with the saved data. You can reset it and restart the app. Note: after resetting, you need to restart the app twice.", + buttonText: "Restart App", + secondButtonText: "Reset Data and Restart App", + onPrimaryClick: { + exitApplication() + }, + onSecondaryClick: { + try! FileManager.default.removeItem(at: OfflineHelper.persistentCachePath) + exitApplication() + } + ) + } +} diff --git a/Sources/EeveeSpotify/Tweak.x.swift b/Sources/EeveeSpotify/Tweak.x.swift index f2d63e51..bb747df3 100644 --- a/Sources/EeveeSpotify/Tweak.x.swift +++ b/Sources/EeveeSpotify/Tweak.x.swift @@ -27,52 +27,49 @@ struct EeveeSpotify: Tweak { do { - let filePath = FileManager.default.urls( - for: .applicationSupportDirectory, in: .userDomainMask - ) - .first! - .appendingPathComponent("PersistentCache") - .appendingPathComponent("offline.bnk") - - if !FileManager.default.fileExists(atPath: filePath.path) { - - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + defer { + NSFileCoordinator.addFilePresenter(OfflineObserver()) + } - PopUpHelper.showPopUp( - message: "Please log in and restart the app to get Premium.", - buttonText: "Okay!" - ) - } + do { + try OfflineHelper.restoreFromEeveeBnk() + NSLog("[EeveeSpotify] Restored from eevee.bnk") - NSLog("[EeveeSpotify] Not activating due to nonexistent file: \(filePath)") return } - let fileData = try Data(contentsOf: filePath) - - let usernameLength = Int(fileData[8]) - let username = Data(fileData[9..<9 + usernameLength]) - - var blankData = try BundleHelper.shared.premiumBlankData() + catch CocoaError.fileReadNoSuchFile { + NSLog("[EeveeSpotify] Not restoring from eevee.bnk: doesn't exist") + } - blankData.insert(UInt8(usernameLength), at: 8) - blankData.insert(contentsOf: username, at: 9) + // - try blankData.write(to: filePath) - NSLog("[EeveeSpotify] Successfully applied") - } + do { + try OfflineHelper.patchOfflineBnk() + try OfflineHelper.backupToEeveeBnk() + } - catch { + catch CocoaError.fileReadNoSuchFile { - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + NSLog("[EeveeSpotify] Not activating: offline.bnk doesn't exist") PopUpHelper.showPopUp( - message: "Unable to apply tweak: \(error)", - buttonText: "OK" + delayed: true, + message: "Please log in and restart the app to get Premium.", + buttonText: "Okay!" ) } + } + catch { + NSLog("[EeveeSpotify] Unable to apply tweak: \(error)") + + PopUpHelper.showPopUp( + delayed: true, + message: "Unable to apply tweak: \(error)", + buttonText: "OK" + ) } } } diff --git a/control b/control index f837f4ed..a6350607 100644 --- a/control +++ b/control @@ -1,6 +1,6 @@ Package: com.eevee.spotify Name: EeveeSpotify -Version: 1.3 +Version: 1.4 Architecture: iphoneos-arm Description: A tweak to get Spotify Premium for free, just like Spotilife Maintainer: Eevee