{"id":20540980,"url":"https://github.com/sentryco/upgradealert","last_synced_at":"2025-04-14T08:39:14.965Z","repository":{"id":160164643,"uuid":"621696721","full_name":"sentryco/UpgradeAlert","owner":"sentryco","description":"🔔 Easily update your app","archived":false,"fork":false,"pushed_at":"2025-03-03T03:48:21.000Z","size":460,"stargazers_count":9,"open_issues_count":9,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-03-27T22:11:33.738Z","etag":null,"topics":["appstore","appupgrade","beta","check-for-update","checkupgrade","checkversion","nsalert","prompt","release","required","security-update","soft-brick","testflight","uialertcontroller","updatewall","upgrade","upgradewall"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sentryco.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-31T07:37:42.000Z","updated_at":"2025-03-03T03:48:24.000Z","dependencies_parsed_at":"2023-06-09T09:30:49.968Z","dependency_job_id":"6c4b7b38-8bb9-4ffb-9605-248a76944b26","html_url":"https://github.com/sentryco/UpgradeAlert","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sentryco%2FUpgradeAlert","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sentryco%2FUpgradeAlert/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sentryco%2FUpgradeAlert/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sentryco%2FUpgradeAlert/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sentryco","download_url":"https://codeload.github.com/sentryco/UpgradeAlert/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248848107,"owners_count":21171304,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appstore","appupgrade","beta","check-for-update","checkupgrade","checkversion","nsalert","prompt","release","required","security-update","soft-brick","testflight","uialertcontroller","updatewall","upgrade","upgradewall"],"created_at":"2024-11-16T01:18:41.689Z","updated_at":"2025-04-14T08:39:14.951Z","avatar_url":"https://github.com/sentryco.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![mit](https://img.shields.io/badge/License-MIT-brightgreen.svg)\n![platform](https://img.shields.io/badge/Platform-iOS/macOS-blue.svg)\n[![SPM compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift)\n[![Tests](https://github.com/sentryco/UpgradeAlert/actions/workflows/Tests.yml/badge.svg)](https://github.com/sentryco/UpgradeAlert/actions/workflows/Tests.yml)\n[![codebeat badge](https://codebeat.co/badges/3cf70bb0-e669-4ad2-b772-e76175cd23c1)](https://codebeat.co/projects/github-com-sentryco-upgradealert-main)\n\n# 🔔 UpgradeAlert\n\n\u003e Easily update your app\n\n## Table of Contents\n- [Problem](#problem)\n- [Solution](#solution)\n- [Screenshots](#screenshots)\n- [Example](#example)\n- [FAQ](#faq)\n- [Gotchas](#gotchas)\n- [Todo](#todo)\n- [License](#license)\n\n### Problem:\n- 🖥 macOS apps do not auto-update by default unless the user has enabled this in the App Store settings.\n- 📲 While iOS apps auto-update by default, some users may have disabled this feature.\n- 🕸 Users may be stuck on an old OS version that is no longer supported.\n- 🪦 Supporting outdated app versions can burden your backend and complicate code maintenance.\n- 🥶 Supporting multiple versions of your app you will result in bloated app code that is hard to iterate on\n- 🤬 Users may complain about issues already fixed in newer versions.\n- 🥵 Outdated apps may lead to negative reviews due to bugs that have been resolved.\n- 🔥 Avoid crashes by ensuring compatibility with the latest device APIs and platform updates.\n- 🚨 Deliver urgent security updates to users promptly.\n\n### Solution:\n- When the current app version is outdated, the user is prompted with a link to the App Store to update.\n- Two different alerts can be displayed: one where the user has the option to update later, and one where the update is mandatory.\n- You can customize the alert title, message, and button texts.\n\n\u003e **Warning**  \n\u003e Setting `isRequired = true` bricks the app until it's updated\n\n### Screenshots:\n\n**For iOS:**\n\n\u003cimg width=\"405\" alt=\"ios\" src=\"iOS.png\"\u003e  \n\n**For macOS:**\n\n\u003cimg width=\"480\" alt=\"ios\" src=\"macOS.png\"\u003e\n\n### Example:\n```swift\nimport UpgradeAlert\n\n// Skip checking for updates if the app is running in beta (e.g., simulator or TestFlight)\nguard !Bundle.isBeta else {\n    Swift.print(\"App is beta or simulator, skip checking for update\")\n    return\n}\n\n// Configure the alert\nUpgradeAlert.config = UAConfig(\n    isRequired: false, // Require users to update\n    alertTitle: \"Update required\", // Alert title\n    alertMessage: { appName, version in \"Version \\(version) is out!\" }, // Alert message\n    laterButtonTitle: \"Later\", // Skip button title\n    updateButtonTitle: \"Update Now\" // Go to App Store button\n)\n\n// Check Apple endpoint to see if there is a new update\nUpgradeAlert.checkForUpdates { outcome in\n    switch outcome {\n    case .error(let error):\n        Swift.print(\"Error: \\(error.localizedDescription)\")\n    case .notNow:\n        Swift.print(\"User chose to update later.\")\n    case .updateNotNeeded:\n        Swift.print(\"App is up-to-date.\")\n    case .didOpenAppStoreToUpdate:\n        Swift.print(\"Redirected user to App Store for update.\")\n    }\n}\n```\n**For debugging**\n\n```swift\n// UA prompt alert test. so we can see how it looks etc.\nUpgradeAlert.showAlert(appInfo: .init(version: \"1.0.1\", trackViewUrl: \"https://apps.apple.com/app/id/com.MyCompany.MyApp\"))\n```\n\n### FAQ:\n\n**Q:** What is an Upgrade-Wall?  \n**A:** An **Upgrade-Wall** (or **Update-Wall**) is a system that prevents mobile app users from using the app if they are still on older versions. It ensures that all users operate on the latest version of the app.\n\n**Q:** Why do we need an Upgrade-Wall?  \n**A:** An Upgrade-Wall is necessary when you need users to update to a new version due to breaking changes, security issues, or to promote new features. For instance:\n\n- **Breaking Changes:** If there are significant changes in the backend API that would cause older versions of the app to crash.\n- **Security Issues:** When older app versions have vulnerabilities that are fixed in newer releases.\n- **Feature Promotion:** To encourage users to experience new features you've introduced.\n\nIn these scenarios, an Upgrade-Wall ensures users update to the latest version, providing a consistent and secure experience.\n\n**Q:** How do you implement an Upgrade-Wall?  \n**A:** An Upgrade-Wall can be implemented using two strategies: **hard** and **soft** Upgrade-Walls.\n\n- **Hard Upgrade-Wall:** Completely restricts users from using the app until they update.\n\n  - Displays a non-dismissible popup with only an **Update** button when the app is opened.\n  - Users cannot skip this popup and must update to continue.\n  - Pressing the **Update** button redirects to the App Store or Play Store to download the latest version.\n\n- **Soft Upgrade-Wall:** Offers flexibility, allowing users to choose whether to update immediately or later.\n\n  - Shows a dismissible popup with options to **Update** or **Skip**.\n  - Users can skip the update and continue using the app.\n  - Encourages but does not force the update.\n\nBoth strategies involve showing a popup or alert to users upon opening the app. You can streamline this process by utilizing existing solutions that provide Upgrade-Wall functionality.\n\n### Gotchas:\n- For macOS `applicationDidBecomeActive` will be called after dismissing the UpgradeAlert, make sure you init UpgradeAlert from another method or else it will create an inescapable loop. This does not apply for iOS.\n\n### Todo:\n- Add screenshot from a test app? ✅ \n- Add support for testflight. There is a repo in issues with a link to another repo that recently added support for this\n- Add country-code to json. en -\u003e english etc. (later)\n- Add localization support\n- Add support for: SKStoreProductViewController allowing the update to be initiated in-app. see https://github.com/rwbutler/Updates/ for code\n- 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\n- Doc params\n- Clean up comments\n- Add support for swiftui\n- Error Handling and Reporting: The current implementation of error handling in various parts of the codebase could be improved for better clarity and functionality. For instance: UpgradeAlert.swift: The method checkForUpdates uses a simple closure that returns an optional error. This could be enhanced by using a Result type to make the success and error handling paths clearer and more robust. \n- UIAlertController+Ext.swift: The present method does not handle the scenario where there is no view controller available to present the alert. This could lead to silent failures in presenting critical update alerts.\n- Refactoring and Code Simplification_ Refactoring some parts of the code could improve readability and maintainability. For example: UpgradeAlert+Variables.swift: The method for generating the request URL could be simplified or made more robust by handling potential errors more gracefully.\n- NSAlert+Ext.swift: The method for presenting alerts in macOS could be refactored to reduce duplication and improve error handling.\n- Testing and Coverage Improving tests to cover edge cases and error scenarios would enhance the reliability of the application. For instance:\n- UpgradeAlertTests.swift: The test cases could be expanded to cover more scenarios, including error handling and user interaction outcomes.\n- Upgrade this to Swift 6.0 (Might be a bit tricky)\n- Add a way to inject text for alert. so we can localize text from the caller etc.as we might want to use .modules with localizations etc\n- Enhance Error Handling with Swift's Result Type\nIssue: The current implementation of asynchronous methods uses custom closures with optional parameters for error handling. This can be improved by leveraging Swift's Result type, which provides a clearer and more structured way to handle success and failure cases.\nImprovement: Refactor asynchronous methods to use Result instead of optional parameters. This will make the code more readable and maintainable.\n\n```swift\npublic final class UpgradeAlert {\n    public static func checkForUpdates(completion: @escaping (Result\u003cVoid, UAError\u003e) -\u003e Void) {\n        DispatchQueue.global(qos: .background).async {\n            getAppInfo { result in\n                switch result {\n                case .success(let appInfo):\n                    let needsUpdate = ComparisonResult.compareVersion(\n                        current: Bundle.version ?? \"0\",\n                        appStore: appInfo.version\n                    ) == .requiresUpgrade\n                    guard needsUpdate else {\n                        completion(.success(()))\n                        return\n                    }\n                    DispatchQueue.main.async {\n                        showAlert(appInfo: appInfo, completion: completion)\n                    }\n                case .failure(let error):\n                    completion(.failure(error))\n                }\n            }\n        }\n    }\n}\n```\n\n- Use Result in getAppInfo Method\nIssue: The getAppInfo method currently uses a closure with optional parameters for error handling.\nImprovement: Modify getAppInfo to use Result\u003cAppInfo, UAError\u003e in its completion handler.\nUpdated Code:\n\n```swift\nprivate static func getAppInfo(completion: @escaping (Result\u003cAppInfo, UAError\u003e) -\u003e Void) {\n    guard let url = requestURL else {\n        completion(.failure(.invalidURL))\n        return\n    }\n    let task = URLSession.shared.dataTask(with: url) { data, _, error in\n        if let error = error {\n            completion(.failure(.invalidResponse(description: error.localizedDescription)))\n            return\n        }\n        guard let data = data else {\n            completion(.failure(.invalidResponse(description: \"No data received\")))\n            return\n        }\n        do {\n            let result = try JSONDecoder().decode(LookupResult.self, from: data)\n            if let appInfo = result.results.first {\n                completion(.success(appInfo))\n            } else {\n                completion(.failure(.invalidResponse(description: \"No app info available\")))\n            }\n        } catch {\n            completion(.failure(.invalidResponse(description: error.localizedDescription)))\n        }\n    }\n    task.resume()\n}\n```\n\n- Improve NSAlert Presentation\nIssue: In NSAlert+Ext.swift, the code can be refactored to reduce duplication and handle more cases.\nImprovement: Create a general method to \n\n```swift\nextension NSAlert {\n    internal static func present(\n        messageText: String,\n        informativeText: String,\n        style: NSAlert.Style,\n        buttons: [String],\n        completion: ((NSApplication.ModalResponse) -\u003e Void)? = nil\n    ) {\n        let alert = NSAlert()\n        alert.messageText = messageText\n        alert.informativeText = informativeText\n        alert.alertStyle = style\n        buttons.forEach { alert.addButton(withTitle: $0) }\n        if let window = NSApplication.shared.windows.first {\n            alert.beginSheetModal(for: window, completionHandler: completion)\n        } else {\n            print(\"Error: No window available to present alert.\")\n        }\n    }\n}\n```\n- Add Support for SwiftUI Alerts\nIssue: The current implementation does not support SwiftUI, limiting its use in SwiftUI-based apps.\nImprovement: Add methods to present alerts using SwiftUI.\nExample Implementation:\n\n```swift\nimport SwiftUI\n\n@available(iOS 13.0, macOS 10.15, *)\npublic struct UpgradeAlertView: View {\n    @State private var isPresented = false\n    public var body: some View {\n        Text(\"\") // Placeholder\n            .alert(isPresented: $isPresented) {\n                Alert(\n                    title: Text(config.alertTitle),\n                    message: Text(config.alertMessage(nil, appInfo.version)),\n                    primaryButton: .default(Text(config.updateButtonTitle), action: {\n                        // Handle update action\n                    }),\n                    secondaryButton: config.isRequired ? nil : .cancel(Text(config.laterButtonTitle))\n                )\n            }\n            .onAppear {\n                isPresented = true\n            }\n    }\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsentryco%2Fupgradealert","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsentryco%2Fupgradealert","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsentryco%2Fupgradealert/lists"}