Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved comments #7

Merged
merged 4 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
.testTarget(
name: "UpgradeAlertTests",
dependencies: ["UpgradeAlert"])
]
)
]/*,*/
/*swiftLanguageVersions: [.v5],*/
/*url: "https://github.com/sentryco/UpgradeAlert",*/
/*description: "A Swift package for showing upgrade alerts."*/
)

Check warning on line 25 in Package.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Newline Violation: Files should have a single trailing newline (trailing_newline)
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@

> Easily update your app

## Table of Contents
- [Problem](#problem)
- [Solution](#solution)
- [Screenshots](#screenshots)
- [Example](#example)
- [FAQ](#faq)
- [Gotchas](#gotchas)
- [Todo](#todo)
- [License](#license)

### Problem:
- 🖥 macOS app does not auto update by default, unless user has set this specifically in app-store.
- 📲 iOS auto update by default, but a few might have turned it off.
Expand Down Expand Up @@ -86,3 +96,6 @@ UpgradeAlert.showAlert(appInfo: .init(version: "1.0.1", trackViewUrl: "https://a
- Maybe add 1 day delay to showing update alert: to avoid an issue where Apple updates the JSON faster than the app binary propogates to the App Store. https://github.com/amebalabs/AppVersion/blob/master/AppVersion/Source/%20Extensions/Date%2BAppVersion.swift
- Doc params
- Clean up comments

## License
This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file.
33 changes: 27 additions & 6 deletions Sources/UpgradeAlert/UpgradeAlert+Variables.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
*/
extension UpgradeAlert {
/**
* appInfo request url
* - Fixme: ⚠️️ Rename to appInfoRequestURL?
* Computed property to generate the request URL for app information.
* It uses the bundle identifier of the current app to create the URL.
* - Returns: A URL pointing to the app's information on iTunes.
* - Note: This URL might need a country code for region-specific apps.
* - Fixme: ⚠️️ Consider renaming this to appInfoRequestURL for clarity.
*/
internal static var requestURL: URL? {
guard let bundleId: String = Bundle.identifier else { return nil }
Expand All @@ -18,19 +21,37 @@
*/
extension UpgradeAlert {
/**
* - Fixme: ⚠️️ Add appName as a parm as well
* - Fixme: ⚠️️ Rename to UAAlertMessage?
* Typealias for a function that generates an alert message.
* - Parameters:
* - appName: The name of the app. This can be nil.
* - version: The version of the app.
* - Returns: A string that is the alert message.
* - Fixme: ⚠️️ Consider adding appName as a parameter for more flexibility.
* - Fixme: ⚠️️ Consider renaming this to UAAlertMessage for consistency.
*/
public typealias AlertMessage = (_ appName: String?, _ version: String) -> String

Check warning on line 33 in Sources/UpgradeAlert/UpgradeAlert+Variables.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
/**
* Typealias for a completion handler function.
* - Parameters:
* - outcome: The outcome of the operation, encapsulated in a UAOutcome object.
* - Note: This function does not return a value.
*/
public typealias Complete = (_ outcome: UAOutcome) -> Void

Check warning on line 41 in Sources/UpgradeAlert/UpgradeAlert+Variables.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
/**
* Default completion handler function.
* This function simply prints the outcome of the operation.
*/
public static let defaultComplete: Complete = { outcome in Swift.print("default complete - outcome: \(String(describing: outcome))")}

Check warning on line 46 in Sources/UpgradeAlert/UpgradeAlert+Variables.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Closure Spacing Violation: Closure expressions should have a single space inside each brace (closure_spacing)
}
/**
* Setter
*/
extension UpgradeAlert {
/**
* UpgradeAlert config
* Static property to hold the configuration for UpgradeAlert.
* - Note: By default, it uses the default configuration defined in UAConfig.
*/
public static var config: UAConfig = .defaultConfig
}
}

Check warning on line 57 in Sources/UpgradeAlert/UpgradeAlert+Variables.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Newline Violation: Files should have a single trailing newline (trailing_newline)
34 changes: 24 additions & 10 deletions Sources/UpgradeAlert/UpgradeAlert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UIKit
#elseif os(macOS)
import Cocoa
#endif

/**
* - Remark: What if a user doesn't want to update and there is no other option than to update?
* - Remark: If the app always show popup when getting focus. Then activate flight-mode. Launch the app again.
Expand All @@ -22,22 +23,27 @@ extension UpgradeAlert {
* - Remark: Version of the app you want to mark for the update. For example, 1.0.0 // This is the version you want the user to force upgrade to a newer version.
* - Fixme: ⚠️️ Add onAppStoreOpenComplete -> ability to track how many update etc
* - Fixme: ⚠️️ Use Result instead etc
* - Parameter complete: - Fixme: ⚠️️ Add doc
* - Parameter complete: A closure that is called when the update check is complete. It returns an optional AppInfo object and an optional NSError object. If an error occurs during the update check, the NSError object describes the error. If the update check is successful, the AppInfo object contains information about the app.
*/
public static func checkForUpdates(complete: Complete? = defaultComplete) { // complete: (_ appInfo: AppInfo?, error: NSError?)
DispatchQueue.global(qos: .background).async { // Network calls goes on the background thread
// Perform network calls on a background thread
DispatchQueue.global(qos: .background).async {
getAppInfo { appInfo, error in
guard let localVersion: String = Bundle.version, error == nil else { complete?(.error(error: error ?? .bundleErr(desc: "Err, bundle.version"))); return } // guard let currentVersion: String = Bundle.version else { throw NSError(domain: "Err, bundle", code: 0) }
// Fetch app information
guard let localVersion: String = Bundle.version, error == nil else { complete?(.error(error: error ?? .bundleErr(desc: "Err, bundle.version"))); return }
guard let appInfo: AppInfo = appInfo, error == nil else { complete?(.error(error: error ?? .invalidResponse(description: "Err, no appInfo"))); return }
// Check if there is a new version available
let needsUpdate: Bool = ComparisonResult.compareVersion(current: localVersion, appStore: appInfo.version) == .requiresUpgrade
guard needsUpdate else { complete?(.updateNotNeeded); return } // No update needed, don't prompt user
DispatchQueue.main.async { // UI goes on the main thread
// If an update is needed, show an alert on the main thread
DispatchQueue.main.async {
Self.showAlert(appInfo: appInfo, complete: complete)
}
} // Call network
}
}
}
}

/**
* Network
*/
Expand All @@ -49,23 +55,28 @@ extension UpgradeAlert {
* - Note: https://medium.com/usemobile/get-app-version-from-app-store-and-more-e43c5055156a
* - Note: Discussing this solution: https://stackoverflow.com/questions/6256748/check-if-my-app-has-a-new-version-on-appstore
* - Fixme: ⚠️️ Add timeout interval and local cache pollacy: https://github.com/amebalabs/AppVersion/blob/master/AppVersion/Source/AppStoreVersion.swift
* - Parameter completion: - Fixme: ⚠️️ Add doc, and make alias for type
* - Parameter completion: A closure that is called when the app information retrieval is complete. It returns an optional AppInfo object and an optional UAError object. If an error occurs during the retrieval, the UAError object describes the error. If the retrieval is successful, the AppInfo object contains information about the app.
- Fixme: ⚠️ make alias for type
*/
private static func getAppInfo(completion: ((AppInfo?, UAError?) -> Void)?) { /*urlStr: String, */
// Check if the request URL is valid
guard let url: URL = requestURL else { completion?(nil, UAError.invalideURL); return }
// Create a data task to fetch app information
let task = URLSession.shared.dataTask(with: url) { data, _, error in
do {
guard let data = data, error == nil else { throw NSError(domain: error?.localizedDescription ?? "data not available", code: 0) }
let result = try JSONDecoder().decode(LookupResult.self, from: data)
guard let info: AppInfo = result.results.first else { throw NSError(domain: "no app info", code: 0) }
completion?(info, nil)
} catch {
// Handle potential errors during data fetching and decoding
completion?(nil, UAError.invalidResponse(description: error.localizedDescription))
}
}
task.resume()
}
}

/**
* Alert
*/
Expand All @@ -83,25 +94,28 @@ extension UpgradeAlert {
* ## Examples:
* UpgradeAlert.showAlert(appInfo: .init(version: "1.0.1", trackViewUrl: "https://apps.apple.com/app/id/com.MyCompany.MyApp")) // UA prompt alert test. so we can see how it looks etc.
* - Parameters:
* - appInfo: - Fixme: ⚠️️ add doc
* - complete: called after user press cancel or ok button
* - appInfo: An AppInfo object that contains information about the app. This information is used to populate the alert.
* - complete: called after user press cancel or ok button ( A closure that is called when the user interacts with the alert. It returns an optional Complete object that describes the user's action (e.g., whether they chose to update now, update later, or encountered an error)
*/
public static func showAlert(appInfo: AppInfo, complete: Complete? = defaultComplete) { // Fix mark ios
#if os(iOS)
// Create an alert with the app information
let alert = UIAlertController(title: config.alertTitle, message: config.alertMessage(nil, appInfo.version), preferredStyle: .alert)
if !config.isRequired { // aka withConfirmation
// If the update is not required, add a "Not Now" button
let notNowButton = UIAlertAction(title: config.laterButtonTitle, style: .default) { (_: UIAlertAction) in
complete?(.notNow) // not now action
}
alert.addAction(notNowButton)
}
// Add an "Update" button that opens the app's page in the App Store
let updateButton = UIAlertAction(title: config.updateButtonTitle, style: .default) { (_: UIAlertAction) in // update action
guard let appStoreURL: URL = .init(string: appInfo.trackViewUrl) else { complete?(.error(error: .invalidAppStoreURL)); return } // Needed when opeing the app in appstore
guard UIApplication.shared.canOpenURL(appStoreURL) else { Swift.print("err, can't open url"); return }
UIApplication.shared.open(appStoreURL, options: [:], completionHandler: { _ in complete?(.didOpenAppStoreToUpdate) })
}
alert.addAction(updateButton)
// Swift.print("present \(alert)")
// Present the alert
alert.present()
#elseif os(macOS)
NSAlert.present(question: config.alertTitle, text: config.alertMessage(nil, appInfo.version), okTitle: config.updateButtonTitle, cancelTitle: config.isRequired ? nil : config.laterButtonTitle) { answer in
Expand All @@ -113,4 +127,4 @@ extension UpgradeAlert {
}
#endif
}
}
}
6 changes: 6 additions & 0 deletions Sources/UpgradeAlert/ext/Bundle+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import Foundation
* - Fixme: ⚠️️ there is also displayName
*/
extension Bundle {
// The name of the main bundle
internal static let name: String? = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
// The version of the main bundle
internal static let version: String? = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
// The build number of the main bundle
internal static let build: String? = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String
// The identifier of the main bundle
internal static let identifier: String? = Bundle.main.object(forInfoDictionaryKey: kCFBundleIdentifierKey as String) as? String

/**
* Tests if an app is in beta
* Description: determines if beta from receipt
Expand All @@ -17,6 +22,7 @@ extension Bundle {
* - Note: If it fails there might be a fix here: https://stackoverflow.com/a/52232267/5389500
* - Note: Seems like this can determine if an app is beta: https://developer.apple.com/documentation/appstoreconnectapi/list_all_builds_of_an_app
* - Fixme: ⚠️️ Here is code that checks if something is testflight: https://gist.github.com/lukaskubanek/cbfcab29c0c93e0e9e0a16ab09586996
* fix. we could make BundleError enum BundleError: Error { case appStoreReceiptURLNotFound }
*/
public static var isBeta: Bool { // was named: isSimulatorOrTestFlight
guard let path = Bundle.main.appStoreReceiptURL?.path else {
Expand Down
23 changes: 16 additions & 7 deletions Sources/UpgradeAlert/ext/NSAlert+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import Cocoa

extension NSAlert {
/**
* Presents a warning alert to the user with customizable text, buttons, and completion handler.
*
* ## Examples:
* NSAlert.present(question: "Ok?", text: "Choose your answer.") { answer in
* print(answer)
* }
* Presents an alert to the user.
*
* - Parameters:
* - message: The main text of the alert.
* - title: The title of the alert.
* - question: The main text of the alert.
* - text: The informative text of the alert.
* - okTitle: The text for the OK button. Defaults to "OK".
* - cancelTitle: The text for the cancel button. Defaults to "Cancel".
* - view: The view to present the alert from. If nil, the alert is presented from the first window of the application.
Expand All @@ -21,23 +23,30 @@ extension NSAlert {
alert.messageText = question
alert.informativeText = text
alert.alertStyle = .warning

// Add OK button to the alert
if let okTitle = okTitle { alert.addButton(withTitle: okTitle) }

// Add Cancel button to the alert
if let cancelTitle = cancelTitle { alert.addButton(withTitle: cancelTitle) }

// Present the alert from the provided view's window, or from the first window of the application if no view is provided
if let win: NSWindow = view?.window ?? NSApplication.shared.windows.first {
// Begin the alert and handle the user's response in the completion handler
alert.beginSheetModal(for: win, completionHandler: { (modalResponse: NSApplication.ModalResponse) -> Void in
if modalResponse == .alertFirstButtonReturn {
complete?(true)
complete?(true) // User clicked OK
} else {
complete?(false)
complete?(false) // User clicked Cancel or closed the alert
}
})
} else {
Swift.print("no window")
Swift.print("no window") // No window was available to present the alert
}
}
}
#endif
//if anAlert.runModal() == .alertFirstButtonReturn {
// return .terminateNow
//}
//return .terminateLater
//return .terminateLater
1 change: 1 addition & 0 deletions Sources/UpgradeAlert/ext/UIAlertController+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UIKit
extension UIAlertController {
/**
* Present alert
* fix: throw error if vc is not available?
*/
internal func present() {
Self.presentedOrRootVC?.present(self, animated: true, completion: nil)
Expand Down
12 changes: 8 additions & 4 deletions Sources/UpgradeAlert/ext/UIApplication+Ext.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
#if os(iOS)
import UIKit

/**
* Window and status-bar (iOS 15 etc)
* Extension to UIApplication to handle window and status-bar related functionalities (iOS 15 etc).
*/
extension UIApplication {
/**
* - Remark: Key Scene can be found by doing `keyWin.windowScene`
* Key window property.
* - Remark: The key scene can be found by accessing `keyWin.windowScene`.
* - Returns: The key window if it exists, otherwise nil.
*/
internal var keyWin: UIWindow? {
self
.connectedScenes
// Flatten the array of windows from each UIWindowScene
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
// Find the first window that is the key window
.first { $0.isKeyWindow }
}
}
#endif

#endif
24 changes: 17 additions & 7 deletions Sources/UpgradeAlert/helper/AppInfo.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import Foundation

/**
* - Remark: it's possible to get more info like rating, release date, release notes etc etc see here: https://github.com/amebalabs/AppVersion/blob/master/AppVersion/Source/AppStoreVersion.swift
* - Fixme: ⚠️️ Rename to UAAppInfo?
* - Fixme: ⚠️️ Add doc
* This struct represents the basic information about an application.
* It includes the current version of the application and the URL to the application's page on the App Store.
*
* - Remark: More information such as rating, release date, release notes etc. can be fetched. For more details, see: https://github.com/amebalabs/AppVersion/blob/master/AppVersion/Source/AppStoreVersion.swift
* - Fixme: ⚠️️ Consider renaming this struct to UAAppInfo for better clarity.
* - Fixme: ⚠️️ Add more detailed documentation for this struct and its properties.
*/
public struct AppInfo: Decodable {
// The current version of the application.
public let version: String
public let trackViewUrl: String // make this optional? since macOS might not use it?

// The URL to the application's page on the App Store.
// This might be optional for macOS applications as they might not have an App Store page.
public let trackViewUrl: String

/**
* Initializes a new instance of `AppInfo`.
* - Parameters:
* - version: - Fixme: ⚠️️ add doc
* - trackViewUrl: - Fixme: ⚠️️ add doc
* - version: The current version of the application.
* - trackViewUrl: The URL to the application's page on the App Store.
*/
public init(version: String, trackViewUrl: String) {
self.version = version
self.trackViewUrl = trackViewUrl
}
}
}
11 changes: 9 additions & 2 deletions Sources/UpgradeAlert/helper/LookupResult.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import Foundation

/**
* - Fixme: ⚠️️ add doc, used for parsing apple app info I think
* `LookupResult` is a struct that conforms to the `Decodable` protocol.
* It is used to parse the JSON response from the Apple App Store API.
* The parsed data is stored in the `results` array, which contains instances of `AppInfo`.
*
* - Note: This struct is part of the `UpgradeAlert` module, specifically under the `helper` directory.
*/
struct LookupResult: Decodable {
// `results` is an array of `AppInfo` instances.
// Each `AppInfo` instance represents the information of an app retrieved from the Apple App Store API.
let results: [AppInfo]
}
}
Loading
Loading