├── .github ├── CODEOWNERS ├── .syft.yaml ├── workflows │ └── dependency-submission.yml ├── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml └── pull_request_template.md ├── Tests └── PrinceOfVersionsTests │ ├── mockdata │ ├── invalid_update_no_json.json │ ├── valid_update_only_min_version.json │ ├── valid_update_no_notification.json │ ├── invalid_update_no_min_version.json │ ├── invalid_update_optional_without_version.json │ ├── invalid_update_no_ios.json │ ├── malformed_json.json │ ├── valid_update_notification_once.json │ ├── valid_update_full_with_metadata_null.json │ ├── requirementChecks │ │ ├── valid_update_no_requirements.json │ │ ├── valid_update_with_decreasing_requirements.json │ │ ├── valid_update_with_shuffled_requirements.json │ │ └── valid_update_with_increasing_requirements.json │ ├── valid_update_only_v2_ios.json │ ├── valid_update_only_v2_macos.json │ ├── valid_update_only_v2_metadata_empty.json │ ├── valid_update_full.json │ ├── valid_update_full_with_metadata_empty.json │ ├── valid_update_full_with_metadata.json │ ├── valid_update_full_with_metadata_malformed.json │ └── app_store_version_example.json │ ├── Info.plist │ ├── VersionTest.swift │ ├── PrinceOfVersionsTest.swift │ ├── UpdateInfoTest.swift │ └── RequirementsTest.swift ├── include └── module.modulemap ├── PrinceOfVersionsSample ├── PrinceOfVersionsIosSample │ ├── Supporting Files │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── 120.png │ │ │ │ ├── 180.png │ │ │ │ ├── 40.png │ │ │ │ ├── 58.png │ │ │ │ ├── 60.png │ │ │ │ ├── 80.png │ │ │ │ ├── 87.png │ │ │ │ ├── 1024.png │ │ │ │ ├── 120-1.png │ │ │ │ └── Contents.json │ │ │ ├── AutomaticCheck.imageset │ │ │ │ ├── icons8-update-50.png │ │ │ │ └── Contents.json │ │ │ ├── Configuration.imageset │ │ │ │ ├── icons8-advanced-search-50-2.png │ │ │ │ └── Contents.json │ │ │ └── Prince.imageset │ │ │ │ ├── s320170302-2113-10bux2s20170302-2113-mebcby.png │ │ │ │ └── Contents.json │ │ ├── PrinceOfVersionsIosSample-Bridging-Header.h │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ ├── AppConfiguration.xcconfig │ ├── ObjectiveC Implementation │ │ └── ConfigurationViewController │ │ │ ├── ObjCConfigurationViewController.h │ │ │ └── ObjCConfigurationViewController.m │ ├── Constants.swift │ ├── AppDelegate.swift │ └── Swift Implementation │ │ └── ConfigurationViewController.swift ├── PrinceOfVersionsIosSample.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── iOS-Sample.xcscheme └── README.md ├── PrinceOfVersionsMacSample ├── PrinceOfVersionsMacSample │ ├── Supporting Files │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── 128.png │ │ │ │ ├── 16.png │ │ │ │ ├── 256.png │ │ │ │ ├── 32.png │ │ │ │ ├── 512.png │ │ │ │ ├── 64.png │ │ │ │ ├── 1024.png │ │ │ │ ├── 256-1.png │ │ │ │ ├── 32-1.png │ │ │ │ ├── 512-1.png │ │ │ │ └── Contents.json │ │ ├── PrinceOfVersionsMacSample-Bridging-Header.h │ │ ├── PrinceOfVersionsMacSample.entitlements │ │ └── Info.plist │ ├── AppConfiguration.xcconfig │ ├── ObjectiveC Implementation │ │ └── ConfigurationViewController │ │ │ ├── ObjCConfigurationController.h │ │ │ └── ObjCConfigurationController.m │ ├── Constants.swift │ ├── AppDelegate.swift │ └── Swift Implementation │ │ └── ConfigurationController.swift ├── README.md └── PrinceOfVersionsMacSample.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── macOS-Sample.xcscheme ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── PrinceOfVersions.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── PrinceOfVersions_Info.plist ├── PrinceOfVersionsTests_Info.plist └── xcshareddata │ └── xcschemes │ ├── PrinceOfVersionsTests.xcscheme │ └── PrinceOfVersions.xcscheme ├── PrinceOfVersions.xcworkspace ├── xcshareddata │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── Sources └── PrinceOfVersions │ ├── SupportingFiles │ ├── PrinceOfVersions.h │ ├── PrivacyInfo.xcprivacy │ └── Info.plist │ ├── ResponseModels │ ├── BaseUpdateData.swift │ ├── AppStoreUpdateData │ │ ├── AppStoreUpdateResult.swift │ │ └── AppStoreUpdateInfo.swift │ └── UpdateData │ │ ├── UpdateResult.swift │ │ └── UpdateInfo.swift │ ├── Objective-C Helpers │ ├── ResponseModel Wrapers │ │ ├── AppStoreData │ │ │ ├── AppStoreUpdateInfoObjectiveC.swift │ │ │ └── AppStoreUpdateResultObjectiveC.swift │ │ └── UpdateData │ │ │ ├── UpdateResultObjectiveC.swift │ │ │ └── UpdateInfoObjectiveC.swift │ └── Objective-C Extensions │ │ ├── ObjectiveCBaseExtensions.swift │ │ ├── CheckUpdatesObjectiveCExtensions.swift │ │ └── CheckUpdateFromAppStoreObjectiveCExtensions.swift │ ├── PoVDataTypes │ ├── NotificationType.swift │ ├── PoVError.swift │ ├── PoVRequestOptions.swift │ └── Version.swift │ └── Common │ ├── ConfigurationData.swift │ └── Utility │ └── AnyDecodable.swift ├── Package.swift ├── PrinceOfVersions.podspec ├── SECURITY.md ├── prince-of-versions.svg ├── .swiftlint.yml ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── PoV 4.0 Migration Guide.md ├── .gitignore └── JSON.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @Filip2Stojanovski -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/invalid_update_no_json.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/.syft.yaml: -------------------------------------------------------------------------------- 1 | select-catalogers: ["swift", "cocoapods", "spm"] 2 | -------------------------------------------------------------------------------- /include/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module PrinceOfVersions { 2 | umbrella header "PrinceOfVersions.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/PrinceOfVersionsIosSample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/PrinceOfVersionsMacSample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/120-1.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/256-1.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/32-1.png -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/512-1.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AutomaticCheck.imageset/icons8-update-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AutomaticCheck.imageset/icons8-update-50.png -------------------------------------------------------------------------------- /PrinceOfVersions.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Configuration.imageset/icons8-advanced-search-50-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Configuration.imageset/icons8-advanced-search-50-2.png -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Prince.imageset/s320170302-2113-10bux2s20170302-2113-mebcby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infinum/ios-prince-of-versions/HEAD/PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Prince.imageset/s320170302-2113-10bux2s20170302-2113-mebcby.png -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Prince.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "s320170302-2113-10bux2s20170302-2113-mebcby.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_only_min_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "minimum_version": "1.2.3" 4 | }, 5 | "android": { 6 | "minimum_version": "1.2.3", 7 | "latest_version": { 8 | "version": "2.4.5", 9 | "notification_type": "ALWAYS" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/AppConfiguration.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // App.xcconfig 3 | // PrinceOfVersionsSample 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | // Configuration settings file format documentation can be found at: 10 | // https://help.apple.com/xcode/#/dev745c5c974 11 | 12 | APP_VERSION = 2.0.0 13 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/AppConfiguration.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // AppConfiguration.xcconfig 3 | // PrinceOfVersionsMacSample 4 | // 5 | // Created by Jasmin Abou Aldan on 20/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | // Configuration settings file format documentation can be found at: 10 | // https://help.apple.com/xcode/#/dev745c5c974 11 | 12 | APP_VERSION = 2.0.0 13 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_no_notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "minimum_version": "1.2.3", 4 | "latest_version": { 5 | "version": "2.4.5" 6 | } 7 | }, 8 | "android": { 9 | "minimum_version": "1.2.3", 10 | "latest_version": { 11 | "version": "2.4.5", 12 | "notification_type": "ALWAYS" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/invalid_update_no_min_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "latest_version": { 4 | "version": "2.4.5", 5 | "notification_type": "ALWAYS" 6 | } 7 | }, 8 | "android": { 9 | "minimum_version": "1.2.3", 10 | "latest_version": { 11 | "version": "2.4.5", 12 | "notification_type": "ONCE" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/invalid_update_optional_without_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "minimum_version": "1.2.3", 4 | "latest_version": { 5 | "notification_type": "ALWAYS" 6 | } 7 | }, 8 | "android": { 9 | "minimum_version": "1.2.3", 10 | "latest_version": { 11 | "version": "2.4.5", 12 | "notification_type": "ONCE" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/PrinceOfVersionsMacSample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/invalid_update_no_ios.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_ios": { 3 | "minimum_version": "1.2.3", 4 | "latest_version": { 5 | "version": "2.4.5", 6 | "notification_type": "ALWAYS" 7 | } 8 | }, 9 | "android": { 10 | "minimum_version": "1.2.3", 11 | "latest_version": { 12 | "version": "2.4.5", 13 | "notification_type": "ONCE" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/malformed_json.json: -------------------------------------------------------------------------------- 1 | { 2 | not_ios: { 3 | not_minimum_version: "1.2.3", 4 | not_latest_version: { 5 | not_version: "2.4.5", 6 | not_notification_type: "ALWAYS" 7 | } 8 | }, 9 | not_android: { 10 | not_minimum_version: "1.2.3", 11 | not_latest_version": { 12 | not_version: "2.4.5", 13 | not_notification_type: "ONCE" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_notification_once.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "minimum_version": "1.2.3", 4 | "latest_version": { 5 | "version": "2.4.5", 6 | "notification_type": "ONCE" 7 | } 8 | }, 9 | "android": { 10 | "minimum_version": "1.2.3", 11 | "latest_version": { 12 | "version": "2.4.5", 13 | "notification_type": "ALWAYS" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_full_with_metadata_null.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "minimum_version": "1.2.3", 4 | "latest_version": { 5 | "version": "2.4.5", 6 | "notification_type": "ALWAYS" 7 | } 8 | }, 9 | "android": { 10 | "minimum_version": "1.2.3", 11 | "latest_version": { 12 | "version": "2.4.5", 13 | "notification_type": "ONCE" 14 | } 15 | }, 16 | "meta": null 17 | } 18 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/ObjectiveC Implementation/ConfigurationViewController/ObjCConfigurationController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCConfigurationController.h 3 | // PrinceOfVersionsMacSample 4 | // 5 | // Created by Jasmin Abou Aldan on 20/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ObjCConfigurationController : NSViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/ObjectiveC Implementation/ConfigurationViewController/ObjCConfigurationViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC-ConfigurationViewController.h 3 | // PrinceOfVersionsSample 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ObjCConfigurationViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /.github/workflows/dependency-submission.yml: -------------------------------------------------------------------------------- 1 | name: iOS Dependency Submission 2 | on: 3 | push: 4 | branches: [ 'master' ] 5 | 6 | workflow_dispatch: 7 | 8 | permissions: 9 | actions: read 10 | contents: write 11 | 12 | jobs: 13 | dependency-report: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Generate and Submit SBOM 19 | uses: anchore/sbom-action@v0 20 | with: 21 | config: .github/.syft.yaml 22 | path: . 23 | format: spdx-json 24 | dependency-snapshot: true 25 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AutomaticCheck.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icons8-update-50.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // PrinceOfVersionsSample 4 | // 5 | // Created by Jasmin Abou Aldan on 14/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Constants { 12 | static let princeOfVersionsURL = "https://pastebin.com/raw/0MfYmWGu" 13 | } 14 | 15 | @objcMembers 16 | class Constant: NSObject { 17 | 18 | private override init() {} 19 | 20 | static var princeOfVersionsURL: String { 21 | return Constants.princeOfVersionsURL 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // PrinceOfVersionsMacSample 4 | // 5 | // Created by Jasmin Abou Aldan on 20/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Constants { 12 | static let princeOfVersionsURL = "https://pastebin.com/raw/0MfYmWGu" 13 | } 14 | 15 | @objcMembers 16 | class Constant: NSObject { 17 | 18 | private override init() {} 19 | 20 | static var princeOfVersionsURL: String { 21 | return Constants.princeOfVersionsURL 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/Configuration.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icons8-advanced-search-50-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/SupportingFiles/PrinceOfVersions.h: -------------------------------------------------------------------------------- 1 | // 2 | // Versioner.h 3 | // Versioner 4 | // 5 | // Created by Jasmin Abou Aldan on 21/06/16. 6 | // Copyright © 2016 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Versioner. 12 | FOUNDATION_EXPORT double VersionerVersionNumber; 13 | 14 | //! Project version string for Versioner. 15 | FOUNDATION_EXPORT const unsigned char VersionerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/SupportingFiles/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | 10 | NSPrivacyAccessedAPIType 11 | NSPrivacyAccessedAPICategoryUserDefaults 12 | NSPrivacyAccessedAPITypeReasons 13 | 14 | CA92.1 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Propose a new feature or an idea for this project. 3 | labels: enhancement 4 | body: 5 | - type: textarea 6 | id: feature-description 7 | attributes: 8 | label: Feature description 9 | description: A clear and concise description of the feature request, including what problem it solves. 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: additional-information 14 | attributes: 15 | label: Additional information 16 | description: An additional information or screenshots about the feature request. 17 | validations: 18 | required: false 19 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/requirementChecks/valid_update_no_requirements.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos":[ 3 | { 4 | "required_version":"10.10.0", 5 | "last_version_available":"11.0", 6 | "notify_last_version_frequency":"ALWAYS" 7 | }, 8 | { 9 | "required_version":"9.1", 10 | "last_version_available":"11.0", 11 | "notify_last_version_frequency":"ALWAYS" 12 | }, 13 | { 14 | "required_version":"9.0", 15 | "last_version_available":"11.0", 16 | "notify_last_version_frequency":"ONCE" 17 | } 18 | ], 19 | "meta":{ 20 | "key3":true, 21 | "key4":"value2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/ResponseModels/BaseUpdateData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseUpdateData.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 02/06/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | @objc 11 | public enum UpdateStatus: Int { 12 | case noUpdateAvailable 13 | case requiredUpdateNeeded 14 | case newUpdateAvailable 15 | } 16 | 17 | public protocol BaseUpdateResult { 18 | associatedtype BaseInfo: BaseUpdateInfo 19 | var updateVersion: Version { get } 20 | var updateState: UpdateStatus { get } 21 | var updateInfo: BaseInfo { get } 22 | } 23 | 24 | public protocol BaseUpdateInfo { 25 | var lastVersionAvailable: Version? { get } 26 | var installedVersion: Version { get } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/PrinceOfVersions_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PrinceOfVersions", 8 | platforms: [ 9 | .macOS(.v10_13), 10 | .iOS(.v11) 11 | ], 12 | products: [ 13 | .library( 14 | name: "PrinceOfVersions", 15 | type: .dynamic, 16 | targets: ["PrinceOfVersions"] 17 | ) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "PrinceOfVersions", 22 | dependencies: [], 23 | resources: [.copy("SupportingFiles/PrivacyInfo.xcprivacy")] 24 | ), 25 | .testTarget( 26 | name: "PrinceOfVersionsTests", 27 | dependencies: ["PrinceOfVersions"] 28 | ) 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/PrinceOfVersionsTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PrinceOfVersions.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "PrinceOfVersions" 3 | s.version = "4.0.7" 4 | s.summary = "Library checks for updates using configuration from some resource." 5 | s.homepage = "https://github.com/infinum/ios-prince-of-versions" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Jasmin Abou Aldan" => "jasmin.aboualdan@infinum.hr" } 8 | s.platform = :ios, :osx 9 | s.ios.deployment_target = '11.0' 10 | s.osx.deployment_target = '10.13' 11 | s.source = { :git => "https://github.com/infinum/ios-prince-of-versions.git", :tag => "#{s.version}" } 12 | s.source_files = "Sources/**/*.{h,m,swift}" 13 | s.resource_bundles = { 'PrinceOfVersions' => ['Sources/PrinceOfVersions/SupportingFiles/PrivacyInfo.xcprivacy'] } 14 | s.ios.framework = 'UIKit' 15 | s.osx.framework = 'AppKit' 16 | s.swift_version = "5.1" 17 | end 18 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_only_v2_ios.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios":[ 3 | { 4 | "required_version":"10.10.0", 5 | "last_version_available":"11.0", 6 | "notify_last_version_frequency":"ALWAYS", 7 | "requirements":{ 8 | "required_os_version":"10.12.1" 9 | } 10 | }, 11 | { 12 | "required_version":"9.1", 13 | "last_version_available":"11.0", 14 | "notify_last_version_frequency":"ALWAYS", 15 | "requirements":{ 16 | "required_os_version":"10.11.1", 17 | "region":"hr", 18 | "bluetooth":"5.0" 19 | } 20 | }, 21 | { 22 | "required_version":"9.0", 23 | "last_version_available":"11.0", 24 | "notify_last_version_frequency":"ONCE", 25 | "requirements":{ 26 | "required_os_version":"10.14.2", 27 | "region":"us" 28 | } 29 | } 30 | ], 31 | "meta":null 32 | } 33 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_only_v2_macos.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos":[ 3 | { 4 | "required_version":"10.10.0", 5 | "last_version_available":"11.0", 6 | "notify_last_version_frequency":"ALWAYS", 7 | "requirements":{ 8 | "required_os_version":"10.12.1" 9 | } 10 | }, 11 | { 12 | "required_version":"9.1", 13 | "last_version_available":"11.0", 14 | "notify_last_version_frequency":"ALWAYS", 15 | "requirements":{ 16 | "required_os_version":"10.11.1", 17 | "region":"hr", 18 | "bluetooth":"5.0" 19 | } 20 | }, 21 | { 22 | "required_version":"9.0", 23 | "last_version_available":"11.0", 24 | "notify_last_version_frequency":"ONCE", 25 | "requirements":{ 26 | "required_os_version":"10.14.2", 27 | "region":"us" 28 | } 29 | } 30 | ], 31 | "meta":{ 32 | "key3":true, 33 | "key4":"value2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PrinceOfVersionsMacSample 4 | // 5 | // Created by Jasmin Abou Aldan on 20/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | // Insert code here to initialize your application 16 | 17 | // Uncomment version that you want to build: 18 | createAndShowViewController(with: "SwiftAppSample") 19 | // createAndShowViewController(with: "ObjCAppSample") 20 | } 21 | } 22 | 23 | private extension AppDelegate { 24 | 25 | func createAndShowViewController(with identifier: String) { 26 | let viewController = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: identifier) as! NSViewController 27 | NSApplication.shared.mainWindow?.contentViewController = viewController 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/requirementChecks/valid_update_with_decreasing_requirements.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos":[ 3 | { 4 | "required_version":"9.0", 5 | "last_version_available":"11.0", 6 | "notify_last_version_frequency":"ONCE", 7 | "requirements":{ 8 | "required_os_version":"10.14.2", 9 | "region":"us", 10 | "bluetooth":"5.0" 11 | } 12 | }, 13 | { 14 | "required_version":"9.1", 15 | "last_version_available":"11.0", 16 | "notify_last_version_frequency":"ALWAYS", 17 | "requirements":{ 18 | "required_os_version":"10.11.1", 19 | "region":"hr" 20 | } 21 | }, 22 | { 23 | "required_version":"10.10.0", 24 | "last_version_available":"11.0", 25 | "notify_last_version_frequency":"ALWAYS", 26 | "requirements":{ 27 | "required_os_version":"10.12.1" 28 | } 29 | } 30 | ], 31 | "meta":{ 32 | "key3":true, 33 | "key4":"value2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/requirementChecks/valid_update_with_shuffled_requirements.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos":[ 3 | { 4 | "required_version":"9.1", 5 | "last_version_available":"11.0", 6 | "notify_last_version_frequency":"ALWAYS", 7 | "requirements":{ 8 | "required_os_version":"10.11.1", 9 | "region":"hr" 10 | } 11 | }, 12 | { 13 | "required_version":"9.0", 14 | "last_version_available":"11.0", 15 | "notify_last_version_frequency":"ONCE", 16 | "requirements":{ 17 | "required_os_version":"10.14.2", 18 | "region":"us", 19 | "bluetooth":"5.0" 20 | } 21 | }, 22 | { 23 | "required_version":"10.10.0", 24 | "last_version_available":"11.0", 25 | "notify_last_version_frequency":"ALWAYS", 26 | "requirements":{ 27 | "required_os_version":"10.12.1" 28 | } 29 | } 30 | ], 31 | "meta":{ 32 | "key3":true, 33 | "key4":"value2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/requirementChecks/valid_update_with_increasing_requirements.json: -------------------------------------------------------------------------------- 1 | { 2 | "macos":[ 3 | { 4 | "required_version":"10.10.0", 5 | "last_version_available":"11.0", 6 | "notify_last_version_frequency":"ALWAYS", 7 | "requirements":{ 8 | "required_os_version":"10.12.1" 9 | } 10 | }, 11 | { 12 | "required_version":"9.1", 13 | "last_version_available":"11.0", 14 | "notify_last_version_frequency":"ALWAYS", 15 | "requirements":{ 16 | "required_os_version":"10.11.1", 17 | "region":"hr", 18 | } 19 | }, 20 | { 21 | "required_version":"9.0", 22 | "last_version_available":"11.0", 23 | "notify_last_version_frequency":"ONCE", 24 | "requirements":{ 25 | "required_os_version":"10.14.2", 26 | "region":"us", 27 | "bluetooth":"5.0" 28 | } 29 | }, 30 | ], 31 | "meta":{ 32 | "key3":true, 33 | "key4":"value2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting security issues 4 | 5 | At Infinum we are committed to ensuring the security of our software. If you have discovered a security vulnerability or have concerns regarding the security of our project, we encourage you to report it to us in a responsible manner. 6 | 7 | If you discover a security vulnerability, please report it to us by emailing us at opensource@infinum.com. We will review your report, and if the issue is confirmed, we will work to resolve the issue as soon as possible and coordinate the release of a security patch. 8 | 9 | ## Responsible disclosure 10 | 11 | We request that you practice responsible disclosure by allowing us time to investigate and address any reported vulnerabilities before making them public. We believe this approach helps protect our users and provides a better outcome for everyone involved. 12 | 13 | ## Preferred languages 14 | 15 | We prefer all communication to be in English. 16 | 17 | ## Contributions 18 | 19 | We greatly appreciate your help in keeping Infinum projects secure. Your efforts contribute to the ongoing improvement of our project's security. 20 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PrinceOfVersionsSample 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | 19 | // Uncomment version that you want to build: 20 | createAndShowViewController(with: "SwiftAppSample") 21 | // createAndShowViewController(with: "ObjCAppSample") 22 | 23 | return true 24 | } 25 | } 26 | 27 | private extension AppDelegate { 28 | 29 | func createAndShowViewController(with identifier: String) { 30 | let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: identifier) 31 | window?.rootViewController = viewController 32 | window?.makeKeyAndVisible() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Prince of Versions 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(APP_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2019 infinum. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/ResponseModel Wrapers/AppStoreData/AppStoreUpdateInfoObjectiveC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreUpdateInfoObjectiveC.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 02/06/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | @objc(AppStoreUpdateInfo) 11 | @objcMembers 12 | public class __ObjCAppStoreUpdateInfo: NSObject { 13 | 14 | // MARK: - Private properties 15 | 16 | private var appStoreInfo: AppStoreUpdateInfo 17 | 18 | // MARK: - Init 19 | 20 | internal init(from appStoreInfo: AppStoreUpdateInfo) { 21 | self.appStoreInfo = appStoreInfo 22 | } 23 | } 24 | 25 | // MARK: - Public properties - 26 | 27 | extension __ObjCAppStoreUpdateInfo: BaseUpdateInfo { 28 | 29 | /// Returns latest available version of the app. 30 | public var lastVersionAvailable: Version? { 31 | return appStoreInfo.lastVersionAvailable 32 | } 33 | 34 | /// Returns installed version of the app. 35 | public var installedVersion: Version { 36 | return appStoreInfo.installedVersion 37 | } 38 | } 39 | 40 | extension __ObjCAppStoreUpdateInfo { 41 | 42 | /// Returns latest version release date. 43 | public var releaseDate: Date? { 44 | return appStoreInfo.releaseDate 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /prince-of-versions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon / 80px / ic-princeofversions 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/PoVDataTypes/NotificationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationType.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 28/05/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Swift - Public notification type - 11 | 12 | /** 13 | Returns update status notification frequency. 14 | 15 | Possible values are: `Once` and `Always`. 16 | 17 | If `NotificationType` is **once**, only first time when new app update is available, `updateStatus` will be `.newUpdateAvailable`, each subsequent call, `updateStatus` value is going to be `.noUpdateAvailable`. 18 | 19 | If `NotificationType` is **always**, `updateStatus` will always return `.newUpdateAvailable` if new optional app update is available. 20 | */ 21 | public enum NotificationType: String, Codable { 22 | case always = "ALWAYS" 23 | case once = "ONCE" 24 | 25 | enum CodingKeys: CodingKey { 26 | case always 27 | case once 28 | } 29 | } 30 | 31 | // MARK: - Objective-C Public notification type - 32 | 33 | @objc 34 | public enum UpdateNotificationType: Int { 35 | case once 36 | case always 37 | } 38 | 39 | // MARK: - Internal helpers - 40 | 41 | internal extension NotificationType { 42 | 43 | var updateNotificationType: UpdateNotificationType { 44 | switch self { 45 | case .always: return .always 46 | case .once: return .once 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "120-1.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "1024x1024", 53 | "idiom" : "ios-marketing", 54 | "filename" : "1024.png", 55 | "scale" : "1x" 56 | } 57 | ], 58 | "info" : { 59 | "version" : 1, 60 | "author" : "xcode" 61 | } 62 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 6 | 7 | **Related issue**: 8 | 9 | ## Changes 10 | 11 | ### Type 12 | 13 | - [ ] **Feature**: This pull request introduces a new feature. 14 | - [ ] **Bug fix**: This pull request fixes a bug. 15 | - [ ] **Refactor**: This pull request refactors existing code. 16 | - [ ] **Documentation**: This pull request updates documentation. 17 | - [ ] **Other**: This pull request makes other changes. 18 | 19 | #### Additional information 20 | 21 | - [ ] This pull request introduces a **breaking change**. 22 | 23 | ### Description 24 | 25 | 30 | 31 | ## Checklist 32 | 33 | - [ ] I have performed a self-review of my own code. 34 | - [ ] I have tested my changes, including edge cases. 35 | - [ ] I have added necessary tests for the changes introduced (if applicable). 36 | - [ ] I have updated the documentation to reflect my changes (if applicable). 37 | 38 | ## Additional notes 39 | 40 | 43 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/ResponseModel Wrapers/UpdateData/UpdateResultObjectiveC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateResultObjectiveC.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(UpdateResult) 12 | @objcMembers 13 | public class __ObjCUpdateResult: NSObject { 14 | 15 | // MARK: - Private properties 16 | 17 | private var updateResult: UpdateResult 18 | 19 | // MARK: - Init 20 | 21 | init(from updateResult: UpdateResult) { 22 | self.updateResult = updateResult 23 | } 24 | } 25 | 26 | // MARK: - Public wrappers - 27 | 28 | extension __ObjCUpdateResult: BaseUpdateResult { 29 | 30 | /// The biggest version it is possible to update to, or current version of the app if it isn't possible to update 31 | public var updateVersion: Version { 32 | return updateResult.updateVersion 33 | } 34 | 35 | /// Resolution of the update check 36 | public var updateState: UpdateStatus { 37 | return updateResult.updateState 38 | } 39 | 40 | /// Update configuration values used to check 41 | @objc 42 | public var updateInfo: __ObjCUpdateInfo { 43 | return __ObjCUpdateInfo(from: updateResult.updateInfoData) 44 | } 45 | } 46 | 47 | extension __ObjCUpdateResult { 48 | 49 | /// Merged metadata from JSON 50 | public var metadata: [String : Any]? { 51 | return updateResult.metadata 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "32-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "256-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "512-1.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - missing_docs 3 | - valid_docs 4 | - function_parameter_count 5 | - line_length 6 | - trailing_whitespace 7 | - force_cast 8 | - force_unwrapping 9 | - nesting 10 | - shorthand_operator 11 | - identifier_name 12 | - type_body_length 13 | - file_length 14 | - large_tuple 15 | - switch_case_alignment 16 | - force_try 17 | - valid_ibinspectable 18 | - colon 19 | whitelist_rules: 20 | excluded: # paths to ignore during linting. Takes precedence over `included`. 21 | - Carthage 22 | - Pods 23 | - Source/ExcludedFolder 24 | - Source/ExcludedFile.swift 25 | # configurable rules can be customized from this configuration file 26 | # binary rules can set their severity level 27 | force_cast: warning # implicitly 28 | force_unwrapping: warning # implicitly 29 | force_try: 30 | severity: warning # explicitly 31 | # rules that have both warning and error levels, can set just the warning level 32 | # implicitly 33 | line_length: 510 34 | # they can set both implicitly with an array 35 | function_body_length: 36 | - 100 37 | - 150 38 | # or they can set both explicitly 39 | file_length: 40 | warning: 400 41 | error: 700 42 | # naming rules can set warnings/errors for min_length and max_length 43 | # additionally they can set excluded names 44 | type_name: 45 | min_length: 1 # only warning 46 | max_length: # warning and error 47 | warning: 60 48 | error: 70 49 | excluded: iPhone # excluded via string 50 | variable_name: 51 | min_length: 2 52 | max_length: 60 53 | cyclomatic_complexity: 54 | warning: 20 55 | error: 25 56 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle) 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report. 3 | labels: bug 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: A clear and concise description of what the bug is. 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: environment 14 | attributes: 15 | label: Environment 16 | description: | 17 | An environment information where issue occurred. Try to provide as much information as possible, including: 18 | - device name, model and manufacturer 19 | - operating system version 20 | - software name, version and build number 21 | - additional information (e.g. dependencies, IDE, etc.) 22 | value: | 23 | - Device: 24 | - Operating system: 25 | - Software information: 26 | - Additional information: 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: reproduction-steps 31 | attributes: 32 | label: Reproduction steps 33 | description: Steps to reproduce the behavior. 34 | value: | 35 | 1. 36 | 2. 37 | 3. 38 | ... 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: expected-behavior 43 | attributes: 44 | label: Expected behavior 45 | description: A clear and concise description of what you expected to happen. 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: additional-information 50 | attributes: 51 | label: Additional information 52 | description: Any additional information that might be helpful in solving the issue. 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PoV 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/ResponseModel Wrapers/UpdateData/UpdateInfoObjectiveC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateInfoObjectiveC.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 29/05/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | @objc(UpdateInfo) 11 | @objcMembers 12 | public class __ObjCUpdateInfo: NSObject { 13 | 14 | // MARK: - Private properties 15 | 16 | private var updateInfo: UpdateInfo 17 | private var updateNotificationType: UpdateNotificationType 18 | 19 | // MARK: - Init 20 | 21 | init(from updateInfo: UpdateInfo) { 22 | self.updateInfo = updateInfo 23 | self.updateNotificationType = updateInfo.notificationType.updateNotificationType 24 | } 25 | } 26 | 27 | // MARK: - Public wrappers - 28 | 29 | // Should be updated with new properties from UpdateInfo 30 | 31 | extension __ObjCUpdateInfo: BaseUpdateInfo { 32 | 33 | /// Returns latest available version of the app. 34 | public var lastVersionAvailable: Version? { 35 | return updateInfo.lastVersionAvailable 36 | } 37 | 38 | /// Returns installed version of the app. 39 | public var installedVersion: Version { 40 | return updateInfo.installedVersion 41 | } 42 | } 43 | 44 | extension __ObjCUpdateInfo { 45 | 46 | /// Returns minimum required version of the app. 47 | public var requiredVersion: Version? { 48 | return updateInfo.requiredVersion 49 | } 50 | 51 | /// Returns requirements for configuration. 52 | public var requirements: [String : Any]? { 53 | return updateInfo.requirements 54 | } 55 | 56 | /// Returns notification frequency for configuration. 57 | public var notificationType: UpdateNotificationType { 58 | return updateNotificationType 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_only_v2_metadata_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios":[ 3 | { 4 | "required_version":"1.2.3", 5 | "last_version_available":"1.9.0", 6 | "notify_last_version_frequency":"ALWAYS", 7 | "requirements":{ 8 | "required_os_version":"8.0.0", 9 | "region":"hr", 10 | "bluetooth":"5.0" 11 | }, 12 | "meta":{ 13 | "key1":"value1", 14 | "key2":2 15 | } 16 | }, 17 | { 18 | "required_version":"1.2.3", 19 | "last_version_available":"2.4.5", 20 | "notify_last_version_frequency":"ALWAYS", 21 | "requirements":{ 22 | "required_os_version":"12.1.2" 23 | }, 24 | "meta":{ 25 | "key3":"value3" 26 | } 27 | } 28 | ], 29 | "macos":[ 30 | { 31 | "required_version":"10.10.0", 32 | "last_version_available":"11.0", 33 | "notify_last_version_frequency":"ALWAYS", 34 | "requirements":{ 35 | "required_os_version":"10.12.1" 36 | } 37 | }, 38 | { 39 | "required_version":"9.1", 40 | "last_version_available":"11.0", 41 | "notify_last_version_frequency":"ALWAYS", 42 | "requirements":{ 43 | "required_os_version":"10.11.1", 44 | "region":"hr", 45 | "bluetooth":"5.0" 46 | } 47 | }, 48 | { 49 | "required_version":"9.0", 50 | "last_version_available":"11.0", 51 | "notify_last_version_frequency":"ONCE", 52 | "requirements":{ 53 | "required_os_version":"10.14.2", 54 | "region":"us" 55 | } 56 | } 57 | ], 58 | "meta":{ 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/ResponseModel Wrapers/AppStoreData/AppStoreUpdateResultObjectiveC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreUpdateResultObjectiveC.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(AppStoreUpdateResult) 12 | @objcMembers 13 | public class __ObjCAppStoreResult: NSObject { 14 | 15 | // MARK: - Private properties 16 | 17 | private var updateResult: AppStoreUpdateResult 18 | 19 | // MARK: - Init 20 | 21 | init(from updateResult: AppStoreUpdateResult) { 22 | self.updateResult = updateResult 23 | } 24 | } 25 | 26 | // MARK: - Public wrappers - 27 | 28 | // Should be updated with new properties from UpdateInfo 29 | 30 | extension __ObjCAppStoreResult: BaseUpdateResult { 31 | 32 | /// The biggest version it is possible to update to, or current version of the app if it isn't possible to update 33 | public var updateVersion: Version { 34 | return updateResult.updateVersion 35 | } 36 | 37 | /// Resolution of the update check 38 | public var updateState: UpdateStatus { 39 | return updateResult.updateState 40 | } 41 | 42 | /// Update configuration values used to check 43 | @objc 44 | public var updateInfo: __ObjCAppStoreUpdateInfo { 45 | return __ObjCAppStoreUpdateInfo(from: updateResult.updateInfoData) 46 | } 47 | } 48 | 49 | extension __ObjCAppStoreResult { 50 | 51 | /** 52 | Returns bool value if phased release period is in progress. 53 | 54 | __WARNING:__ As we are not able to determine if phased release period is finished earlier (release to all options is selected after a while), `phaseReleaseInProgress` will return `false` only after 7 days of `currentVersionReleaseDate` value send by `itunes.apple.com` API. 55 | */ 56 | public var phaseReleaseInProgress: Bool { 57 | return updateResult.phaseReleaseInProgress 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_full.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios":{ 3 | "minimum_version":"1.2.3", 4 | "minimum_version_min_sdk":"8.0.0", 5 | "latest_version":{ 6 | "version":"2.4.5", 7 | "notification_type":"ALWAYS", 8 | "min_sdk":"12.1.2" 9 | } 10 | }, 11 | "ios2":[ 12 | { 13 | "required_version":"1.2.3", 14 | "last_version_available":"1.9.0", 15 | "notify_last_version_frequency":"ALWAYS", 16 | "requirements":{ 17 | "required_os_version":"8.0.0", 18 | "region":"hr", 19 | "bluetooth":"5.0" 20 | }, 21 | "meta":{ 22 | "key1":"value1", 23 | "key2":2 24 | } 25 | }, 26 | { 27 | "required_version":"1.2.3", 28 | "last_version_available":"2.4.5", 29 | "notify_last_version_frequency":"ALWAYS", 30 | "requirements":{ 31 | "required_os_version":"12.1.2" 32 | }, 33 | "meta":{ 34 | "key3":"value3", 35 | } 36 | } 37 | ], 38 | "macos":[ 39 | { 40 | "required_version":"10.10.0", 41 | "last_version_available":"11.0", 42 | "notify_last_version_frequency":"ALWAYS", 43 | "requirements":{ 44 | "required_os_version":"10.12.1" 45 | } 46 | }, 47 | { 48 | "required_version":"9.1", 49 | "last_version_available":"11.0", 50 | "notify_last_version_frequency":"ALWAYS", 51 | "requirements":{ 52 | "required_os_version":"10.11.1", 53 | "region":"hr", 54 | "bluetooth":"5.0" 55 | } 56 | }, 57 | { 58 | "required_version":"9.0", 59 | "last_version_available":"11.0", 60 | "notify_last_version_frequency":"ONCE", 61 | "requirements":{ 62 | "required_os_version":"10.14.2", 63 | "region":"us" 64 | } 65 | } 66 | ], 67 | "meta": null 68 | } 69 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_full_with_metadata_empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios":{ 3 | "minimum_version":"1.2.3", 4 | "minimum_version_min_sdk":"8.0.0", 5 | "latest_version":{ 6 | "version":"2.4.5", 7 | "notification_type":"ALWAYS", 8 | "min_sdk":"12.1.2" 9 | } 10 | }, 11 | "ios2":[ 12 | { 13 | "required_version":"1.2.3", 14 | "last_version_available":"1.9.0", 15 | "notify_last_version_frequency":"ALWAYS", 16 | "requirements":{ 17 | "required_os_version":"8.0.0", 18 | "region":"hr", 19 | "bluetooth":"5.0" 20 | }, 21 | "meta":{ 22 | "key1":"value1", 23 | "key2":2 24 | } 25 | }, 26 | { 27 | "required_version":"1.2.3", 28 | "last_version_available":"2.4.5", 29 | "notify_last_version_frequency":"ALWAYS", 30 | "requirements":{ 31 | "required_os_version":"12.1.2" 32 | }, 33 | "meta":{ 34 | "key3":"value3", 35 | } 36 | } 37 | ], 38 | "macos":[ 39 | { 40 | "required_version":"10.10.0", 41 | "last_version_available":"11.0", 42 | "notify_last_version_frequency":"ALWAYS", 43 | "requirements":{ 44 | "required_os_version":"10.12.1" 45 | } 46 | }, 47 | { 48 | "required_version":"9.1", 49 | "last_version_available":"11.0", 50 | "notify_last_version_frequency":"ALWAYS", 51 | "requirements":{ 52 | "required_os_version":"10.11.1", 53 | "region":"hr", 54 | "bluetooth":"5.0" 55 | } 56 | }, 57 | { 58 | "required_version":"9.0", 59 | "last_version_available":"11.0", 60 | "notify_last_version_frequency":"ONCE", 61 | "requirements":{ 62 | "required_os_version":"10.14.2", 63 | "region":"us" 64 | } 65 | } 66 | ], 67 | "meta":{ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_full_with_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios":{ 3 | "minimum_version":"1.2.3", 4 | "minimum_version_min_sdk":"8.0.0", 5 | "latest_version":{ 6 | "version":"2.4.5", 7 | "notification_type":"ALWAYS", 8 | "min_sdk":"12.1.2" 9 | } 10 | }, 11 | "ios2":[ 12 | { 13 | "required_version":"1.2.3", 14 | "last_version_available":"1.9.0", 15 | "notify_last_version_frequency":"ALWAYS", 16 | "requirements":{ 17 | "required_os_version":"8.0.0", 18 | "region":"hr", 19 | "bluetooth":"5.0" 20 | }, 21 | "meta":{ 22 | "key1":"value1", 23 | "key2":2 24 | } 25 | }, 26 | { 27 | "required_version":"1.2.3", 28 | "last_version_available":"2.4.5", 29 | "notify_last_version_frequency":"ALWAYS", 30 | "requirements":{ 31 | "required_os_version":"12.1.2" 32 | }, 33 | "meta":{ 34 | "key3":"value3", 35 | } 36 | } 37 | ], 38 | "macos":[ 39 | { 40 | "required_version":"10.10.0", 41 | "last_version_available":"11.0", 42 | "notify_last_version_frequency":"ALWAYS", 43 | "requirements":{ 44 | "required_os_version":"10.12.1" 45 | } 46 | }, 47 | { 48 | "required_version":"9.1", 49 | "last_version_available":"11.0", 50 | "notify_last_version_frequency":"ALWAYS", 51 | "requirements":{ 52 | "required_os_version":"10.11.1", 53 | "region":"hr", 54 | "bluetooth":"5.0" 55 | } 56 | }, 57 | { 58 | "required_version":"9.0", 59 | "last_version_available":"11.0", 60 | "notify_last_version_frequency":"ONCE", 61 | "requirements":{ 62 | "required_os_version":"10.14.2", 63 | "region":"us" 64 | } 65 | } 66 | ], 67 | "meta":{ 68 | "key3":true, 69 | "key4":"value2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/valid_update_full_with_metadata_malformed.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios":{ 3 | "minimum_version":"1.2.3", 4 | "minimum_version_min_sdk":"8.0.0", 5 | "latest_version":{ 6 | "version":"2.4.5", 7 | "notification_type":"ALWAYS", 8 | "min_sdk":"12.1.2" 9 | } 10 | }, 11 | "ios2":[ 12 | { 13 | "required_version":"1.2.3", 14 | "last_version_available":"1.9.0", 15 | "notify_last_version_frequency":"ALWAYS", 16 | "requirements":{ 17 | "required_os_version":"8.0.0", 18 | "region":"hr", 19 | "bluetooth":"5.0" 20 | }, 21 | "meta":{ 22 | "key1":"value1", 23 | "key2":2 24 | } 25 | }, 26 | { 27 | "required_version":"1.2.3", 28 | "last_version_available":"2.4.5", 29 | "notify_last_version_frequency":"ALWAYS", 30 | "requirements":{ 31 | "required_os_version":"12.1.2" 32 | }, 33 | "meta":{ 34 | "key3":"value3", 35 | } 36 | } 37 | ], 38 | "macos":[ 39 | { 40 | "required_version":"10.10.0", 41 | "last_version_available":"11.0", 42 | "notify_last_version_frequency":"ALWAYS", 43 | "requirements":{ 44 | "required_os_version":"10.12.1" 45 | } 46 | }, 47 | { 48 | "required_version":"9.1", 49 | "last_version_available":"11.0", 50 | "notify_last_version_frequency":"ALWAYS", 51 | "requirements":{ 52 | "required_os_version":"10.11.1", 53 | "region":"hr", 54 | "bluetooth":"5.0" 55 | } 56 | }, 57 | { 58 | "required_version":"9.0", 59 | "last_version_available":"11.0", 60 | "notify_last_version_frequency":"ONCE", 61 | "requirements":{ 62 | "required_os_version":"10.14.2", 63 | "region":"us" 64 | } 65 | } 66 | ], 67 | "meta":{ 68 | "key3":value1, 69 | "key4":value2 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/xcshareddata/xcschemes/PrinceOfVersionsTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Common/ConfigurationData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationData.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 16/04/2020. 6 | // Copyright © 2020 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ConfigurationData - 12 | 13 | struct ConfigurationData: Decodable { 14 | let requiredVersion: Version? 15 | let lastVersionAvailable: Version? 16 | let notifyLastVersionFrequency: NotificationType? 17 | let requirements: Requirements? 18 | let meta: [String: AnyDecodable]? 19 | } 20 | 21 | // MARK: - Requirements - 22 | 23 | struct Requirements: Decodable { 24 | 25 | // MARK: - Internal properties 26 | 27 | let requiredOSVersion: Version? 28 | var userDefinedRequirements: [String: Any] 29 | 30 | var allRequirements: [String: Any]? { 31 | var requirements = userDefinedRequirements 32 | if let requiredOSVersion = requiredOSVersion { 33 | requirements.updateValue(requiredOSVersion, forKey: CodingKeys.requiredOSVersion.rawValue) 34 | } 35 | return requirements 36 | } 37 | 38 | // MARK: - Init 39 | 40 | init(from decoder: Decoder) throws { 41 | 42 | let container = try decoder.container(keyedBy: CodingKeys.self) 43 | requiredOSVersion = try? container.decode(Version.self, forKey: .requiredOSVersion) 44 | 45 | userDefinedRequirements = [:] 46 | 47 | let dynamicKeysContainer = try decoder.container(keyedBy: DynamicKey.self) 48 | 49 | dynamicKeysContainer.allKeys.forEach { 50 | guard 51 | $0.stringValue != CodingKeys.requiredOSVersion.rawValue, 52 | let value = dynamicKeysContainer.getValue(for: $0) 53 | else { return } 54 | 55 | userDefinedRequirements.updateValue(value.value, forKey: $0.stringValue) 56 | } 57 | } 58 | 59 | // MARK: - Coding keys 60 | 61 | enum CodingKeys: String, CodingKey { 62 | case requiredOSVersion = "requiredOsVersion" 63 | } 64 | } 65 | 66 | // MARK: - Helpers - 67 | 68 | private extension KeyedDecodingContainer { 69 | 70 | func getValue(for key: K) -> AnyDecodable? { 71 | return try? decode(AnyDecodable.self, forKey: key) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Welcome to our project! We appreciate your interest in helping us improve it. 4 | 5 | ## How can I contribute? 6 | 7 | There are multiple ways in which you can help us make this project even better. 8 | 9 | - Reporting bugs or suggesting new features 10 | - Contributing code improvements or new features 11 | - Writing, updating, or fixing tests 12 | - Improving documentation, including inline comments, user manuals, and developer guides 13 | 14 | ## Issue reporting 15 | 16 | If you found a bug or have an idea for a new feature, please open an issue. Be sure to include a clear and descriptive title, as well as a detailed description of the bug or feature. 17 | 18 | To avoid duplicate issues, please check if a similar issue has already been created. 19 | 20 | ## Making changes 21 | 22 | To make changes to the project, please follow these steps: 23 | 24 | 1. Fork the project repository. 25 | 2. Create a new branch for your changes, based on the project's main branch. 26 | 3. Make your changes. Ensure you've followed the coding style and standards. 27 | 4. Test your changes thoroughly, ensuring all existing tests pass and new tests cover your changes where appropriate. 28 | 5. Commit your changes with a clear and descriptive commit message. 29 | 6. Push your changes to your fork. 30 | 7. Create a pull request to the project's main branch. 31 | 32 | Once we check everything, we will merge the changes into the main branch and include it in the next release. 33 | 34 | ## Guidelines for pull requests 35 | 36 | When submitting a pull request, please ensure that: 37 | 38 | - Your pull request is concise and well-scoped 39 | - Your code is properly tested 40 | - Your code adheres to the project's coding standards and style guidelines 41 | - Your commit message is clear and descriptive 42 | - Your pull request includes a description of the changes you have made and why you have made them 43 | 44 | Try to avoid creating large pull requests that include multiple unrelated changes. Instead, break them down into smaller, more focused pull requests. This will make it easier for us to review and merge your changes. 45 | 46 | ## Code of conduct 47 | 48 | We want to ensure a welcoming environment for everyone. With that in mind, all contributors are expected to follow our [code of conduct](/CODE_OF_CONDUCT.md). 49 | 50 | ## License 51 | 52 | By submitting a pull request you agree to release that code under the project's [license](/LICENSE). 53 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/README.md: -------------------------------------------------------------------------------- 1 | # Prince of Versions macOS Sample App 2 | 3 | ## Features 4 | 5 | * Check info set in https://pastebin.com/raw/ZAfWNZCi 6 | * Check app configuration 7 | 8 | ## Usage 9 | 10 | You'll find 2 ViewControllers from where you can check how Prince of Versions could be used. In `ConfigurationController` you'll get all informations stored on server as well as current version of the app, while in `AutomaticCheckController` you'll only get an info if update is available and if update is mandatory or optional. 11 | 12 | You can change the app version from `AppConfiguration.xcconfig` file and Swift/Objective-C version of the `ViewControllers` from the `AppDelegate`. 13 | 14 | 1. Getting all data 15 | 16 | Used in `ConfigurationController`. 17 | 18 | ```swift 19 | let url = URL(string: "https://pastebin.com/raw/ZAfWNZCi") 20 | PrinceOfVersions().loadConfiguration(from: url) { response in 21 | switch response.result { 22 | case .success(let info): 23 | print("Minimum version: ", info.minimumRequiredVersion) 24 | print("Installed version: ", info.installedVersion) 25 | print("Is minimum version satisfied: ", info.isMinimumVersionSatisfied) 26 | print("Notification type: ", info.notificationType) 27 | 28 | if let latestVersion = info.latestVersion { 29 | print("Is minimum version satisfied: ", latestVersion) 30 | } 31 | case .failure(let error): 32 | print(error.localizedDescription) 33 | } 34 | } 35 | ``` 36 | 37 | 2. Automatic handling update frequency 38 | 39 | Used in `AutomaticCheckController`. 40 | 41 | ```swift 42 | let url = URL(string: "https://pastebin.com/raw/ZAfWNZCi") 43 | PrinceOfVersions().checkForUpdates(from: url, 44 | newVersion: { (latestVersion, isMinimumVersionSatisfied, metadata) in 45 | ... 46 | }, 47 | noNewVersion: { (isMinimumVersionSatisfied, metadata) in 48 | ... 49 | }, 50 | error: { error in 51 | ... 52 | }) 53 | ``` 54 | 55 | ### Contributing 56 | 57 | Feedback and code contributions are very much welcome. Just make a pull request with a short description of your changes. By making contributions to this project you give permission for your code to be used under the same [license](https://github.com/infinum/Android-prince-of-versions/blob/dev/LICENCE). 58 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/README.md: -------------------------------------------------------------------------------- 1 | # Prince of Versions iOS Sample App 2 | 3 | ## Features 4 | 5 | * Check info set in https://pastebin.com/raw/ZAfWNZCi 6 | * Check app configuration 7 | 8 | ## Usage 9 | 10 | You'll find 2 ViewControllers from where you can check how Prince of Versions could be used. In `ConfigurationViewController` you'll get all informations stored on server as well as current version of the app, while in `AutomaticCheckViewController` you'll only get an info if update is available and if update is mandatory or optional. 11 | 12 | You can change the app version from `AppConfiguration.xcconfig` file and Swift/Objective-C version of the `ViewControllers` from the `AppDelegate`. 13 | 14 | 1. Getting all data 15 | 16 | Used in `ConfigurationViewController`. 17 | 18 | ```swift 19 | let url = URL(string: "https://pastebin.com/raw/ZAfWNZCi") 20 | PrinceOfVersions().loadConfiguration(from: url) { response in 21 | switch response.result { 22 | case .success(let info): 23 | print("Minimum version: ", info.minimumRequiredVersion) 24 | print("Installed version: ", info.installedVersion) 25 | print("Is minimum version satisfied: ", info.isMinimumVersionSatisfied) 26 | print("Notification type: ", info.notificationType) 27 | 28 | if let latestVersion = info.latestVersion { 29 | print("Is minimum version satisfied: ", latestVersion) 30 | } 31 | case .failure(let error): 32 | print(error.localizedDescription) 33 | } 34 | } 35 | ``` 36 | 37 | 2. Automatic handling update frequency 38 | 39 | Used in `AutomaticCheckViewController`. 40 | 41 | ```swift 42 | let url = URL(string: "https://pastebin.com/raw/ZAfWNZCi") 43 | PrinceOfVersions().checkForUpdates(from: url, 44 | newVersion: { (latestVersion, isMinimumVersionSatisfied, metadata) in 45 | ... 46 | }, 47 | noNewVersion: { (isMinimumVersionSatisfied, metadata) in 48 | ... 49 | }, 50 | error: { error in 51 | ... 52 | }) 53 | ``` 54 | 55 | ### Contributing 56 | 57 | Feedback and code contributions are very much welcome. Just make a pull request with a short description of your changes. By making contributions to this project you give permission for your code to be used under the same [license](https://github.com/infinum/Android-prince-of-versions/blob/dev/LICENCE). 58 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/ResponseModels/AppStoreUpdateData/AppStoreUpdateResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreUpdateResult.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 02/06/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AppStoreUpdateResult { 11 | 12 | // MARK: - Private properties 13 | 14 | internal var updateInfoData: AppStoreUpdateInfo 15 | 16 | // MARK: - Init 17 | 18 | init(updateInfo: AppStoreUpdateInfo) { 19 | self.updateInfoData = updateInfo 20 | } 21 | } 22 | 23 | // MARK: - Public properties - 24 | 25 | extension AppStoreUpdateResult: BaseUpdateResult { 26 | 27 | /// The biggest version it is possible to update to, or current version of the app if it isn't possible to update 28 | public var updateVersion: Version { 29 | 30 | guard let latestVersion = updateInfoData.lastVersionAvailable else { 31 | return updateInfoData.installedVersion 32 | } 33 | 34 | return Version.max(latestVersion, updateInfoData.installedVersion) 35 | } 36 | 37 | /** 38 | Resolution of the update check. 39 | 40 | Only possible return values are `.newUpdateAvailable` and `.noUpdateAvailable` since there is no way to determine if the update version is mandatory with AppStore check. 41 | */ 42 | public var updateState: UpdateStatus { 43 | 44 | guard let latestVersion = updateInfoData.lastVersionAvailable else { 45 | return .noUpdateAvailable 46 | } 47 | 48 | let shouldNotify = !latestVersion.wasNotified || updateInfoData.notificationFrequency == .always 49 | 50 | if (latestVersion > updateInfoData.installedVersion) && shouldNotify { 51 | updateInfoData.lastVersionAvailable?.markNotified() 52 | return .newUpdateAvailable 53 | } 54 | 55 | return .noUpdateAvailable 56 | } 57 | 58 | /// Update configuration values used to check 59 | public var updateInfo: AppStoreUpdateInfo { 60 | return updateInfoData 61 | } 62 | } 63 | 64 | extension AppStoreUpdateResult { 65 | 66 | /** 67 | Returns bool value if phased release period is in progress. 68 | 69 | __WARNING:__ As we are not able to determine if phased release period is finished earlier (release to all options is selected after a while), `phaseReleaseInProgress` will return `false` only after 7 days of `currentVersionReleaseDate` value send by `itunes.apple.com` API. 70 | */ 71 | public var phaseReleaseInProgress: Bool { 72 | return updateInfoData.phaseReleaseInProgress 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/VersionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionTest.swift 3 | // PrinceOfVersionsTests 4 | // 5 | // Created by Barbara Vujicic on 20/12/2017. 6 | // Copyright © 2017 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PrinceOfVersions 11 | 12 | class VersionTest: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testEqualMethod() { 25 | let versionOne = try! Version(string: "10") 26 | let versionTwo = try! Version(string: "10.0-0") 27 | let versionThree = try! Version(string: "10.0.0-1") 28 | let versionFour = try! Version(string: "10-0") 29 | 30 | XCTAssertTrue(versionOne == versionTwo) 31 | XCTAssertFalse(versionOne == versionThree) 32 | XCTAssertTrue(versionOne == versionFour) 33 | } 34 | 35 | func testNotEqualMethod() { 36 | let versionOne = try! Version(string: "10") 37 | let versionTwo = try! Version(string: "10.0.0") 38 | let versionThree = try! Version(string: "10.1") 39 | let versionFour = try! Version(string: "10-0") 40 | 41 | XCTAssertFalse(versionOne != versionTwo) 42 | XCTAssertTrue(versionOne != versionThree) 43 | XCTAssertFalse(versionOne != versionFour) 44 | } 45 | 46 | func testGreaterThanMethod() { 47 | let versionOne = try! Version(string: "10.2-3") 48 | let versionTwo = try! Version(string: "10.2") 49 | let versionThree = try! Version(string: "10.2.3") 50 | let versionFour = try! Version(string: "10.1.1-99") 51 | 52 | XCTAssertTrue(versionOne > versionTwo) 53 | XCTAssertFalse(versionOne > versionThree) 54 | XCTAssertTrue(versionOne > versionFour) 55 | } 56 | 57 | func testLessThanMethod() { 58 | let versionOne = try! Version(string: "10.2-3") 59 | let versionTwo = try! Version(string: "10.2") 60 | let versionThree = try! Version(string: "10.2.3") 61 | let versionFour = try! Version(string: "10.1.1-99") 62 | 63 | XCTAssertFalse(versionOne < versionTwo) 64 | XCTAssertTrue(versionOne < versionThree) 65 | XCTAssertFalse(versionOne < versionFour) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/PoVDataTypes/PoVError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PoVError.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 14/09/2019. 6 | // Copyright © 2019 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum PoVError: Error { 12 | case invalidJsonData 13 | case dataNotFound 14 | /// Returns global metadata if available 15 | case requirementsNotSatisfied([String: Any]?) 16 | case missingConfigurationVersion 17 | case invalidCurrentVersion 18 | case invalidBundleId 19 | case unknown(String?) 20 | } 21 | 22 | extension PoVError: LocalizedError { 23 | 24 | public var errorDescription: String? { 25 | switch self { 26 | case .invalidJsonData: 27 | return NSLocalizedString("Invalid JSON Data", comment: "") 28 | case .dataNotFound: 29 | return NSLocalizedString("Data not found for selected app id", comment: "") 30 | case .requirementsNotSatisfied: 31 | return NSLocalizedString("Requirements not satisfied", comment: "") 32 | case .missingConfigurationVersion: 33 | return NSLocalizedString("Missing configuration version", comment: "") 34 | case .invalidCurrentVersion: 35 | return NSLocalizedString("Invalid Current Version", comment: "") 36 | case .invalidBundleId: 37 | return NSLocalizedString("BundleID not found", comment: "") 38 | case .unknown(let customMessage): 39 | guard let message = customMessage else { 40 | return NSLocalizedString("Unknown error", comment: "") 41 | } 42 | return NSLocalizedString(message, comment: "") 43 | } 44 | } 45 | } 46 | 47 | // MARK: - Validation methods - 48 | 49 | extension PoVError { 50 | 51 | static func validate(updateInfo: UpdateInfo) -> PoVError? { 52 | 53 | if updateInfo.configurations == nil { 54 | return .dataNotFound 55 | } 56 | 57 | if updateInfo.configurations != nil && updateInfo.configuration == nil { 58 | return .requirementsNotSatisfied(updateInfo.metadata) 59 | } 60 | 61 | if updateInfo.lastVersionAvailable == nil && updateInfo.requiredVersion == nil { 62 | return .missingConfigurationVersion 63 | } 64 | 65 | if updateInfo.currentInstalledVersion == nil { 66 | return .invalidCurrentVersion 67 | } 68 | 69 | return nil 70 | } 71 | 72 | static func validate(appStoreInfo: AppStoreUpdateInfo) -> PoVError? { 73 | 74 | guard appStoreInfo.results.count > 0 else { return .dataNotFound } 75 | 76 | guard let configuration = appStoreInfo.configurationData else { return .invalidJsonData } 77 | 78 | if configuration.version == nil { return .missingConfigurationVersion } 79 | 80 | if configuration.installedVersion == nil { 81 | return .invalidCurrentVersion 82 | } 83 | 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/ResponseModels/UpdateData/UpdateResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateResult.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 17/04/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Public interface 11 | 12 | public struct UpdateResultResponse { 13 | 14 | /// The server's response to the URL request. 15 | public let response: URLResponse? 16 | 17 | /// The result of response serialization. 18 | public let result: Result 19 | } 20 | 21 | public struct UpdateResult { 22 | 23 | // MARK: - Private properties 24 | 25 | internal var updateInfoData: UpdateInfo 26 | 27 | // MARK: - Init 28 | 29 | init(updateInfo: UpdateInfo, userRequirements: [String : ((Any) -> Bool)] = [:]) { 30 | self.updateInfoData = updateInfo 31 | updateInfoData.userRequirements = userRequirements 32 | } 33 | } 34 | 35 | // MARK: - Public properties - 36 | 37 | extension UpdateResult: BaseUpdateResult { 38 | 39 | /// The biggest version it is possible to update to, or current version of the app if it isn't possible to update 40 | public var updateVersion: Version { 41 | 42 | if let requiredVersion = updateInfoData.requiredVersion, let lastVersionAvailable = updateInfoData.lastVersionAvailable { 43 | return Version.max(requiredVersion, lastVersionAvailable) 44 | } 45 | 46 | if let requiredVersion = updateInfoData.requiredVersion, updateInfoData.lastVersionAvailable == nil { 47 | return Version.max(requiredVersion, updateInfoData.installedVersion) 48 | } 49 | 50 | if updateInfoData.requiredVersion == nil, let lastVersionAvailable = updateInfoData.lastVersionAvailable { 51 | return Version.max(lastVersionAvailable, updateInfoData.installedVersion) 52 | } 53 | 54 | return updateInfoData.installedVersion 55 | } 56 | 57 | /// Resolution of the update check 58 | public var updateState: UpdateStatus { 59 | 60 | if let requiredVersion = updateInfoData.requiredVersion, requiredVersion > updateInfoData.installedVersion { 61 | return .requiredUpdateNeeded 62 | } 63 | 64 | guard let latestVersion = updateInfoData.lastVersionAvailable else { 65 | return .noUpdateAvailable 66 | } 67 | 68 | let shouldNotify = !latestVersion.wasNotified || updateInfoData.notificationType == .always 69 | 70 | if (latestVersion > updateInfoData.installedVersion) && shouldNotify { 71 | updateInfoData.lastVersionAvailable?.markNotified() 72 | return .newUpdateAvailable 73 | } 74 | 75 | return .noUpdateAvailable 76 | } 77 | 78 | /// Update configuration values used to check 79 | public var updateInfo: UpdateInfo { 80 | return updateInfoData 81 | } 82 | } 83 | 84 | extension UpdateResult { 85 | 86 | /// Merged metadata from JSON 87 | public var metadata: [String : Any]? { 88 | return updateInfoData.metadata 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Common/Utility/AnyDecodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyDeodable.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 15/04/2020. 6 | // Copyright © 2020 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct DynamicKey: CodingKey { 12 | 13 | var stringValue: String 14 | init?(stringValue: String) { 15 | self.stringValue = stringValue 16 | } 17 | 18 | var intValue: Int? 19 | init?(intValue: Int) { 20 | return nil 21 | } 22 | } 23 | 24 | public struct AnyDecodable: Decodable { 25 | 26 | public let value: Any 27 | 28 | public init(_ value: T?) { 29 | self.value = value ?? () 30 | } 31 | } 32 | 33 | extension AnyDecodable { 34 | 35 | public init(from decoder: Decoder) throws { 36 | 37 | let container = try decoder.singleValueContainer() 38 | 39 | if container.decodeNil() { 40 | self.init(NSNull()) 41 | } else if let bool = try? container.decode(Bool.self) { 42 | self.init(bool) 43 | } else if let int = try? container.decode(Int.self) { 44 | self.init(int) 45 | } else if let uint = try? container.decode(UInt.self) { 46 | self.init(uint) 47 | } else if let double = try? container.decode(Double.self) { 48 | self.init(double) 49 | } else if let string = try? container.decode(String.self) { 50 | self.init(string) 51 | } else { 52 | throw PoVError.invalidJsonData 53 | } 54 | } 55 | } 56 | 57 | extension AnyDecodable: Equatable { 58 | 59 | public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool { 60 | switch (lhs.value, rhs.value) { 61 | case is (NSNull, NSNull), is (Void, Void): 62 | return true 63 | case let (lhs as Bool, rhs as Bool): 64 | return lhs == rhs 65 | case let (lhs as Int, rhs as Int): 66 | return lhs == rhs 67 | case let (lhs as Int8, rhs as Int8): 68 | return lhs == rhs 69 | case let (lhs as Int16, rhs as Int16): 70 | return lhs == rhs 71 | case let (lhs as Int32, rhs as Int32): 72 | return lhs == rhs 73 | case let (lhs as Int64, rhs as Int64): 74 | return lhs == rhs 75 | case let (lhs as UInt, rhs as UInt): 76 | return lhs == rhs 77 | case let (lhs as UInt8, rhs as UInt8): 78 | return lhs == rhs 79 | case let (lhs as UInt16, rhs as UInt16): 80 | return lhs == rhs 81 | case let (lhs as UInt32, rhs as UInt32): 82 | return lhs == rhs 83 | case let (lhs as UInt64, rhs as UInt64): 84 | return lhs == rhs 85 | case let (lhs as Float, rhs as Float): 86 | return lhs == rhs 87 | case let (lhs as Double, rhs as Double): 88 | return lhs == rhs 89 | case let (lhs as String, rhs as String): 90 | return lhs == rhs 91 | default: 92 | return false 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/Objective-C Extensions/ObjectiveCBaseExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PoVObjC.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 Infinum Ltd. All rights reserved. 7 | // 8 | // Used for exposing Swift methods to the ObjectiveC 9 | // As we don't want to show duplicated methods (one for Swift and one for ObjC) 10 | // simple @available will be used for hiding wrapper methods in Swift. 11 | 12 | import Foundation 13 | 14 | @objc(UpdateResponse) 15 | @objcMembers 16 | public class __ObjCUpdateResponse: NSObject { 17 | 18 | /// The server's response to the URL request. 19 | public let response: URLResponse? 20 | 21 | /// The result of response serialization. 22 | public let result: __ObjCUpdateResult 23 | 24 | public init(response: URLResponse?, result: __ObjCUpdateResult) { 25 | self.response = response 26 | self.result = result 27 | } 28 | } 29 | 30 | // MARK: Helpers 31 | 32 | internal extension PrinceOfVersions { 33 | 34 | // MARK: Check updates 35 | 36 | static func internalyLoadAndPrepareConfiguration( 37 | from URL: URL, 38 | callbackQueue: DispatchQueue, 39 | options: PoVRequestOptions, 40 | completion: @escaping ObjectCompletionBlock, 41 | error: @escaping ObjectErrorBlock 42 | ) -> URLSessionDataTask? { 43 | return PrinceOfVersions.checkForUpdates(from: URL, callbackQueue: callbackQueue, options: options, completion: { response in 44 | switch response.result { 45 | case .success(let updateResult): 46 | let updateResultResponse = __ObjCUpdateResponse( 47 | response: response.response, 48 | result: __ObjCUpdateResult(from: updateResult) 49 | ) 50 | completion(updateResultResponse) 51 | case .failure(let (errorResponse as NSError)): 52 | error(errorResponse) 53 | } 54 | }) 55 | } 56 | 57 | // MARK: AppStore check 58 | 59 | static func internalyCheckAndPrepareForUpdateAppStore( 60 | bundle: Bundle, 61 | trackPhaseRelease: Bool, 62 | callbackQueue: DispatchQueue, 63 | notificationFrequency: NotificationType = .always, 64 | country: String? = nil, 65 | completion: @escaping AppStoreObjectCompletionBlock, 66 | error: @escaping ObjectErrorBlock 67 | ) -> URLSessionDataTask? { 68 | return PrinceOfVersions.checkForUpdateFromAppStore( 69 | trackPhaseRelease: trackPhaseRelease, 70 | bundle: bundle, 71 | callbackQueue: callbackQueue, 72 | notificationFrequency: notificationFrequency, 73 | country: country, 74 | completion: { 75 | result in 76 | switch result { 77 | case .success(let appStoreInfo): 78 | completion(__ObjCAppStoreResult(from: appStoreInfo)) 79 | case .failure(let (errorResponse as NSError)): 80 | error(errorResponse) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | ## Our pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@infinum.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 71 | available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html). 72 | 73 | For answers to common questions about this code of conduct, visit 74 | the [FAQ](https://www.contributor-covenant.org/faq) page. 75 | -------------------------------------------------------------------------------- /PoV 4.0 Migration Guide.md: -------------------------------------------------------------------------------- 1 | # PrinceOfVersions 4.0 Migration Guide 2 | 3 | PrinceOfVersions 4.0 is the latest major release of PrinceOfVersions, library used for checking for updates using configurations from some other source. 4 | 5 | ## Benefits of Upgrading 6 | 7 | * **Multiple configurations:** 8 | 9 | * It is possible to define multiple configuration for the same platform 10 | * Appropriate configuration will be chosen based on the requirements - if defined in JSON 11 | 12 | * **Defining requirements:** 13 | 14 | * Requirements are conditions that have to be met for a configuration to be chosen 15 | * It is not mandatory for a configuration to have requirements 16 | * User can decide whatever requirement they think it's necessary 17 | * `addRequirement` method used to provide requirement check closure 18 | * `required_os_version` built-in support for checking if required OS version requirement is met as long it is defined in JSON 19 | 20 | * **Supporting older versions** 21 | 22 | * If you decide to upgrade PoV to version >= 4.0, both type of users (the ones who have app version with PoV < 4.0 and the ones who have app version with PoV >= 4.0) can be supported with only one JSON, for more information, please check out **JSON Formatting** section. 23 | 24 | ## Breaking Changes 25 | 26 | * **JSON Formatting** 27 | 28 | * JSON formatting has changed, see more [here](JSON.md) 29 | * PoV version 4.0 will only work with the v2 version of JSON. Update the JSON file at the same time with the PoV. 30 | 31 | * **Methods** 32 | 33 | * Both `checkForUpdates` and `loadConfiguration` methods are now unified in one method `checkForUpdates`. 34 | * All methods are now `static`. 35 | * Return type of new `checkForUpdates` method is `UpdateResult` (see more info under **Return types**). 36 | 37 | * Achieving behaviour from old `checkForUpdates` and `loadConfiguration`: 38 | 39 | * Return type `UpdateInfo` in `loadConfiguration` can be found as a property in `UpdateResult` struct. 40 | * Closures that were available in old `checkForUpdates` method have been replaced by `UpdateStatus` enum (see more info under **New Features**) which can also be found in `UpdateResult` struct under property `updateStatus`. 41 | 42 | * **Return types** 43 | 44 | * Each method for checking whether update exists comes with compatible return type (`UpdateResult`, `AppStoreUpdateResult`). 45 | * Each return type, in addition to its essential properties `updateStatus`, `updateVersion`, `updateInfo`, possesses some unique properties specialised for method of getting versioning info. 46 | 47 | * `UpdateResult` 48 | 49 | * New return type which contains all information necessary for the update, to use previous `UpdateInfo` just access `updateInfo` property on returned `UpdateResult` struct. 50 | * Used when getting the versioning information from JSON. 51 | * `metadata` returns global metadata defined in JSON joined with metadata from the chosen configuration. 52 | 53 | * `AppStoreUpdateResult` 54 | 55 | * Used when getting the versioning information from the AppStore Connect. 56 | * `phaseReleaseInProgress` returns bool value if phased release period is in progress. 57 | 58 | ## New Features 59 | 60 | * Added parameter `notificationFrequency` to `checkForUpdateFromAppStore` method which is used for setting desired update notification frequency. 61 | 62 | * **UpdateStatus** 63 | 64 | * New enum which determines if update exists and if it is mandatory. 65 | * Contained in `UpdateResult` struct. 66 | * Replaces closures in old `checkForUpdates` method. 67 | * Possible values are `noUpdateAvailable`, `requiredUpdateNeeded`, `newUpdateAvailable`. 68 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample.xcodeproj/xcshareddata/xcschemes/macOS-Sample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample.xcodeproj/xcshareddata/xcschemes/iOS-Sample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/ResponseModels/AppStoreUpdateData/AppStoreUpdateInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreUpdateInfo.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 06/02/2020. 6 | // Copyright © 2020 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if canImport(UIKit) 12 | import UIKit 13 | #elseif canImport(AppKit) 14 | import AppKit 15 | #endif 16 | 17 | public struct AppStoreUpdateInfo: Codable { 18 | 19 | // MARK: - Internal properties - 20 | 21 | static internal var bundle: Bundle = .main 22 | 23 | internal var notificationFrequency: NotificationType = .always 24 | 25 | internal let results: [ConfigurationData] 26 | 27 | internal var configurationData: ConfigurationData? { 28 | var configurationData = results.first 29 | configurationData?.bundle = AppStoreUpdateInfo.bundle 30 | return configurationData 31 | } 32 | 33 | // MARK: - ConfigData Struct - 34 | 35 | internal struct ConfigurationData: Codable { 36 | 37 | var version: Version? 38 | var minimumOsVersion: Version? 39 | var currentVersionReleaseDate: String? 40 | 41 | var bundle: Bundle = .main 42 | 43 | var installedVersion: Version? { 44 | 45 | guard 46 | let currentVersionString = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, 47 | let currentBuildNumberString = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String 48 | else { 49 | return nil 50 | } 51 | 52 | return try? Version(string: currentVersionString + "-" + currentBuildNumberString) 53 | } 54 | 55 | var releaseDate: Date? { 56 | return currentVersionReleaseDate.flatMap { ConfigurationData.dateFormatter.date(from: $0) } 57 | } 58 | 59 | var sdkVersion: Version? { 60 | #if os(iOS) 61 | return try? Version(string: UIDevice.current.systemVersion) 62 | #elseif os(macOS) 63 | return Version(macVersion: ProcessInfo.processInfo.operatingSystemVersion) 64 | #endif 65 | } 66 | 67 | private static var dateFormatter: DateFormatter = { 68 | let dateFormatter = DateFormatter() 69 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 70 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 71 | return dateFormatter 72 | }() 73 | 74 | private enum CodingKeys: String, CodingKey { 75 | case version, minimumOsVersion, currentVersionReleaseDate 76 | } 77 | } 78 | 79 | internal var phaseReleaseInProgress: Bool { 80 | guard 81 | let releaseDate = configurationData?.releaseDate, 82 | let finishDate = Calendar.current.date(byAdding: .day, value: 7, to: releaseDate) 83 | else { return false } 84 | return finishDate > Date() 85 | } 86 | 87 | // MARK: - CodingKeys - 88 | 89 | enum CodingKeys: String, CodingKey { 90 | case results 91 | } 92 | } 93 | 94 | // MARK: - Public properties - 95 | 96 | extension AppStoreUpdateInfo: BaseUpdateInfo { 97 | 98 | /// Returns latest available version of the app. 99 | public var lastVersionAvailable: Version? { 100 | return configurationData?.version 101 | } 102 | 103 | /// Returns installed version of the app. 104 | public var installedVersion: Version { 105 | guard let version = configurationData?.installedVersion else { 106 | preconditionFailure("Unable to get installed version data") 107 | } 108 | return version 109 | } 110 | } 111 | 112 | extension AppStoreUpdateInfo { 113 | 114 | /// Returns latest version release date. 115 | public var releaseDate: Date? { 116 | return configurationData?.releaseDate 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/PoVDataTypes/PoVRequestOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PoVRequestOptions.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 20/04/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | @objcMembers 11 | public class PoVRequestOptions: NSObject { 12 | 13 | // MARK: - Public properties 14 | 15 | /// Boolean that indicates whether PoV should use security keys from all certificates found in the main bundle. Default value is `false`. 16 | public var shouldPinCertificates: Bool = false 17 | 18 | /// HTTP header fields. 19 | public private(set) var httpHeaderFields: NSMutableDictionary = [:] 20 | 21 | /// Adds value to httpHeaderFields dictionary 22 | @objc(setValue:forHttpHeaderField:) 23 | public func set(value: NSString, httpHeaderField: NSString) { 24 | httpHeaderFields.setObject(value, forKey: httpHeaderField) 25 | } 26 | 27 | // MARK: - Internal properties 28 | 29 | internal var userRequirements: [String: ((Any) -> Bool)] = [:] 30 | 31 | // MARK: - Public methods 32 | 33 | /** 34 | Adds requirement check for configuration. 35 | 36 | Use this method to add custom requirement by which configuration must comply with. 37 | 38 | - parameter key: String that matches key in requirements array in JSON with `requirementsCheck` parameter, 39 | - parameter requirementCheck: A block used to check if a configuration meets the requirement. This block returns `true` if the configuration meets the requirement, and takes the value as input. 40 | 41 | - Warning: Deprecated. Use `addRequirement(key:ofType:requirementCheck:)` instead. 42 | */ 43 | @available(*, deprecated, message: "Use the generic version `addRequirement(key:ofType:requirementCheck:)` instead.") 44 | public func addRequirement( 45 | key: String, 46 | requirementCheck: @escaping ((Any) -> Bool) 47 | ) { 48 | userRequirements.updateValue(requirementCheck, forKey: key) 49 | } 50 | 51 | /** 52 | Adds requirement check for configuration. 53 | 54 | Use this method to add custom requirement by which configuration must comply with. 55 | 56 | - parameter key: String that matches key in requirements array in JSON with `requirementsCheck` parameter, 57 | - parameter type: The expected type of the value. 58 | - parameter requirementCheck: A block used to check if a configuration meets the requirement. This block returns `true` if the configuration meets the requirement, and takes the typed value as input. 59 | 60 | */ 61 | public func addRequirement(key: String, ofType type: T.Type, requirementCheck: @escaping (T) -> Bool) { 62 | userRequirements.updateValue({ value in 63 | guard let typedValue = value as? T else { return false } 64 | return requirementCheck(typedValue) 65 | }, forKey: key) 66 | } 67 | 68 | /** 69 | Adds requirement check for configuration (Objective-C compatible). 70 | 71 | Use this method to add a custom requirement by which configuration must comply with. 72 | 73 | - parameter key: String that matches the key in the requirements array in JSON with the `requirementCheck` parameter. 74 | - parameter type: The expected class of the value (e.g., `NSString.class`). 75 | - parameter requirementCheck: A block used to check if a configuration meets the requirement. This block returns `true` if the configuration meets the requirement, and takes the value as input. 76 | 77 | This method is designed for Objective-C compatibility and uses runtime type checking (`isKindOfClass:`) to validate the value. 78 | */ 79 | @available(swift, obsoleted: 1.0, message: "Use the generic addRequirement(key:ofType:requirementCheck:) method in Swift.") 80 | @objc(addRequirementWithKey:ofType:requirementCheck:) 81 | public func addRequirementWithKey(key: String, ofType type: AnyClass, requirementCheck: @escaping (Any) -> Bool) { 82 | userRequirements.updateValue({ value in 83 | guard let value = value as? NSObject, value.isKind(of: type) else { return false } 84 | return requirementCheck(value) 85 | }, forKey: key) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/ObjectiveC Implementation/ConfigurationViewController/ObjCConfigurationController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCConfigurationController.m 3 | // PrinceOfVersionsMacSample 4 | // 5 | // Created by Jasmin Abou Aldan on 20/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | #import "ObjCConfigurationController.h" 10 | #import "PrinceOfVersionsMacSample-Swift.h" 11 | 12 | @import PrinceOfVersions; 13 | 14 | @interface ObjCConfigurationController () 15 | 16 | @property (nonatomic, weak) IBOutlet NSTextField *updateVersionTextField; 17 | @property (nonatomic, weak) IBOutlet NSTextField *updateStateTextField; 18 | @property (nonatomic, weak) IBOutlet NSTextField *metaTextField; 19 | 20 | @property (nonatomic, weak) IBOutlet NSTextField *requiredVersionTextField; 21 | @property (nonatomic, weak) IBOutlet NSTextField *lastVersionAvailableTextField; 22 | @property (nonatomic, weak) IBOutlet NSTextField *installedVersionTextField; 23 | @property (nonatomic, weak) IBOutlet NSTextField *notificationTypeTextField; 24 | @property (nonatomic, weak) IBOutlet NSTextField *requirementsTextField; 25 | 26 | @end 27 | 28 | @implementation ObjCConfigurationController 29 | 30 | #pragma mark - View Lifecycle 31 | 32 | - (void)viewDidLoad 33 | { 34 | [super viewDidLoad]; 35 | // Do view setup here. 36 | [self checkAppVersion]; 37 | [self checkAppStoreVersion]; 38 | } 39 | 40 | #pragma mark - Private methods 41 | 42 | - (void)checkAppVersion 43 | { 44 | NSURL *princeOfVersionsURL = [NSURL URLWithString:Constant.princeOfVersionsURL]; 45 | 46 | PoVRequestOptions *options = [PoVRequestOptions new]; 47 | [options addRequirementWithKey:@"region" 48 | ofType:[NSString class] 49 | requirementCheck:^BOOL(NSString *value) { 50 | return [value isEqualToString:@"hr"]; 51 | }]; 52 | 53 | [options addRequirementWithKey:@"bluetooth" 54 | ofType:[NSString class] 55 | requirementCheck:^BOOL(NSString *value) { 56 | return [value hasPrefix:@"5"]; 57 | }]; 58 | 59 | __weak __typeof(self) weakSelf = self; 60 | [PrinceOfVersions checkForUpdatesFromURL:princeOfVersionsURL options:options completion:^(UpdateResponse *updateResponse) { 61 | [weakSelf fillUIWithInfoResponse:updateResponse.result]; 62 | } error:^(NSError *error) { 63 | // Handle error 64 | }]; 65 | } 66 | 67 | // In sample app, error will occur as bundle ID 68 | // of the app is not available on the App Store 69 | 70 | - (void)checkAppStoreVersion 71 | { 72 | [PrinceOfVersions checkForUpdateFromAppStoreWithTrackPhasedRelease:NO completion:^(AppStoreUpdateResult *response) { 73 | // Handle success 74 | } error:^(NSError *error) { 75 | // Handle error 76 | }]; 77 | } 78 | 79 | - (void)fillUIWithInfoResponse:(UpdateResult *)infoResponse 80 | { 81 | self.updateVersionTextField.stringValue = infoResponse.updateVersion.description; 82 | self.updateStateTextField.stringValue = [self updateStateFromResult:infoResponse.updateState]; 83 | self.metaTextField.stringValue = infoResponse.metadata.description; 84 | 85 | self.requiredVersionTextField.stringValue = infoResponse.updateInfo.requiredVersion.description; 86 | self.lastVersionAvailableTextField.stringValue = infoResponse.updateInfo.lastVersionAvailable.description; 87 | self.installedVersionTextField.stringValue = infoResponse.updateInfo.installedVersion.description; 88 | self.notificationTypeTextField.stringValue = infoResponse.updateInfo.notificationType == UpdateNotificationTypeOnce ? @"ONCE" : @"ALWAYS"; 89 | self.requirementsTextField.stringValue = infoResponse.updateInfo.requirements.description; 90 | } 91 | 92 | - (NSString *)updateStateFromResult:(UpdateStatus)type 93 | { 94 | switch (type) { 95 | case UpdateStatusNoUpdateAvailable: 96 | return @"No Update Available"; 97 | case UpdateStatusRequiredUpdateNeeded: 98 | return @"Required Update Needed"; 99 | case UpdateStatusNewUpdateAvailable: 100 | return @"New Update Available"; 101 | } 102 | } 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/ObjectiveC Implementation/ConfigurationViewController/ObjCConfigurationViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC-ConfigurationViewController.m 3 | // PrinceOfVersionsSample 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | #import "ObjCConfigurationViewController.h" 10 | #import "PrinceOfVersionsIosSample-Swift.h" 11 | 12 | @import PrinceOfVersions; 13 | 14 | @interface ObjCConfigurationViewController () 15 | 16 | @property (nonatomic, weak) IBOutlet UILabel *updateVersionLabel; 17 | @property (nonatomic, weak) IBOutlet UILabel *updateStateLabel; 18 | @property (nonatomic, weak) IBOutlet UILabel *metaLabel; 19 | 20 | @property (nonatomic, weak) IBOutlet UILabel *requiredVersionLabel; 21 | @property (nonatomic, weak) IBOutlet UILabel *lastVersionAvailableLabel; 22 | @property (nonatomic, weak) IBOutlet UILabel *installedVersionLabel; 23 | @property (nonatomic, weak) IBOutlet UILabel *notificationTypeLabel; 24 | @property (nonatomic, weak) IBOutlet UILabel *requirementsLabel; 25 | 26 | @end 27 | 28 | @implementation ObjCConfigurationViewController 29 | 30 | #pragma mark - View Lifecycle 31 | 32 | - (void)viewDidLoad 33 | { 34 | [super viewDidLoad]; 35 | 36 | // Do any additional setup after loading the view. 37 | [self checkAppVersion]; 38 | [self checkAppStoreVersion]; 39 | } 40 | 41 | #pragma mark - Private methods 42 | 43 | - (void)checkAppVersion 44 | { 45 | NSURL *princeOfVersionsURL = [NSURL URLWithString:Constant.princeOfVersionsURL]; 46 | 47 | PoVRequestOptions *options = [PoVRequestOptions new]; 48 | [options addRequirementWithKey:@"region" 49 | ofType:[NSString class] 50 | requirementCheck:^BOOL(NSString *value) { 51 | // Check OS localisation 52 | return [value isEqualToString:@"hr"]; 53 | }]; 54 | 55 | [options addRequirementWithKey:@"bluetooth" 56 | ofType:[NSString class] 57 | requirementCheck:^BOOL(NSString *value) { 58 | // Check device bluetooth version 59 | return [value hasPrefix:@"5"]; 60 | }]; 61 | 62 | __weak __typeof(self) weakSelf = self; 63 | [PrinceOfVersions checkForUpdatesFromURL:princeOfVersionsURL options:options completion:^(UpdateResponse *updateResponse) { 64 | [weakSelf fillUIWithInfoResponse:updateResponse.result]; 65 | } error:^(NSError *error) { 66 | /* Handle error */ 67 | }]; 68 | } 69 | 70 | // In sample app, error will occur as bundle ID 71 | // of the app is not available on the App Store 72 | 73 | - (void)checkAppStoreVersion 74 | { 75 | [PrinceOfVersions checkForUpdateFromAppStoreWithTrackPhasedRelease:NO completion:^(AppStoreUpdateResult *response) { 76 | // Handle success 77 | } error:^(NSError *error) { 78 | // Handle error 79 | }]; 80 | } 81 | 82 | - (void)fillUIWithInfoResponse:(UpdateResult *)infoResponse 83 | { 84 | self.updateVersionLabel.text = infoResponse.updateVersion.description; 85 | self.updateStateLabel.text = [self updateStateFromResult:infoResponse.updateState]; 86 | self.metaLabel.text = infoResponse.metadata.description; 87 | 88 | UpdateInfo *versionInfo = infoResponse.updateInfo; 89 | 90 | self.requiredVersionLabel.text = 91 | versionInfo.requiredVersion.description; 92 | self.lastVersionAvailableLabel.text = versionInfo.lastVersionAvailable.description; 93 | self.installedVersionLabel.text = versionInfo.installedVersion.description; 94 | self.notificationTypeLabel.text = versionInfo.notificationType == UpdateNotificationTypeOnce ? @"ONCE" : @"ALWAYS"; 95 | self.requirementsLabel.text = versionInfo.requirements.description; 96 | } 97 | 98 | - (NSString *)updateStateFromResult:(UpdateStatus)type 99 | { 100 | switch (type) { 101 | case UpdateStatusNoUpdateAvailable: 102 | return @"No Update Available"; 103 | case UpdateStatusRequiredUpdateNeeded: 104 | return @"Required Update Needed"; 105 | case UpdateStatusNewUpdateAvailable: 106 | return @"New Update Available"; 107 | } 108 | } 109 | 110 | @end 111 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Swift Implementation/ConfigurationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationViewController.swift 3 | // PrinceOfVersionsSample 4 | // 5 | // Created by Jasmin Abou Aldan on 13/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PrinceOfVersions 11 | 12 | class ConfigurationViewController: UIViewController { 13 | 14 | // MARK: - Private properties 15 | // MARK: IBOutlets 16 | 17 | @IBOutlet private weak var updateVersionLabel: UILabel! 18 | @IBOutlet private weak var updateStateLabel: UILabel! 19 | @IBOutlet private weak var metaLabel: UILabel! 20 | 21 | @IBOutlet private weak var requiredVersionLabel: UILabel! 22 | @IBOutlet private weak var lastVersionAvailableLabel: UILabel! 23 | @IBOutlet private weak var installedVersionLabel: UILabel! 24 | @IBOutlet private weak var notificationTypeLabel: UILabel! 25 | @IBOutlet private weak var requirementsLabel: UILabel! 26 | 27 | 28 | // MARK: - View Lifecycle 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | // Do any additional setup after loading the view. 34 | checkAppVersion() 35 | checkAppStoreVersion() 36 | } 37 | } 38 | 39 | // MARK: - Private methods - 40 | 41 | private extension ConfigurationViewController { 42 | 43 | func checkAppVersion() { 44 | 45 | let options = PoVRequestOptions() 46 | 47 | options.addRequirement(key: "region", ofType: String.self) { $0.starts(with: "hr") } 48 | options.addRequirement(key: "bluetooth", ofType: String.self) { $0.starts(with: "5") } 49 | 50 | let princeOfVersionsURL = URL(string: Constants.princeOfVersionsURL)! 51 | 52 | PrinceOfVersions.checkForUpdates( 53 | from: princeOfVersionsURL, 54 | options: options, 55 | completion: { [weak self] response in 56 | switch response.result { 57 | case .success(let infoResponse): 58 | self?.fillUI(with: infoResponse) 59 | case .failure: 60 | // Handle error 61 | break 62 | } 63 | }) 64 | } 65 | 66 | func checkAppStoreVersion() { 67 | // In sample app, error will occur as bundle ID 68 | // of the app is not available on the App Store 69 | PrinceOfVersions.checkForUpdateFromAppStore( 70 | trackPhaseRelease: false, 71 | completion: { result in 72 | switch result { 73 | case .success: 74 | // Handle success 75 | break 76 | case .failure: 77 | // Handle error 78 | break 79 | } 80 | }) 81 | } 82 | } 83 | 84 | private extension ConfigurationViewController { 85 | 86 | func fillUI(with infoResponse: UpdateResult) { 87 | fillUpdateResultUI(with: infoResponse) 88 | fillVersionInfoUI(with: infoResponse.updateInfo) 89 | } 90 | 91 | func fillUpdateResultUI(with infoResponse: UpdateResult) { 92 | updateVersionLabel.text = infoResponse.updateVersion.description 93 | updateStateLabel.text = infoResponse.updateState.updateState 94 | metaLabel.text = "\(infoResponse.metadata ?? [:])" 95 | } 96 | 97 | func fillVersionInfoUI(with versionInfo: UpdateInfo) { 98 | requiredVersionLabel.text = versionInfo.requiredVersion?.description ?? "" 99 | lastVersionAvailableLabel.text = versionInfo.lastVersionAvailable?.description ?? "" 100 | installedVersionLabel.text = versionInfo.installedVersion.description 101 | notificationTypeLabel.text = versionInfo.notificationType == .once ? "ONCE" : "ALWAYS" 102 | requirementsLabel.text = "\(versionInfo.requirements ?? [:])" 103 | } 104 | } 105 | 106 | private extension UpdateStatus { 107 | 108 | var updateState: String { 109 | switch self { 110 | case .noUpdateAvailable: return "No Update Available" 111 | case .requiredUpdateNeeded: return "Required Update Needed" 112 | case .newUpdateAvailable: return "New Update Available" 113 | @unknown default: return "Unkown state" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /PrinceOfVersions.xcodeproj/xcshareddata/xcschemes/PrinceOfVersions.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 77 | 78 | 84 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /PrinceOfVersionsMacSample/PrinceOfVersionsMacSample/Swift Implementation/ConfigurationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationController.swift 3 | // PrinceOfVersionsMacSample 4 | // 5 | // Created by Jasmin Abou Aldan on 20/09/2019. 6 | // Copyright © 2019 infinum. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import PrinceOfVersions 11 | 12 | class ConfigurationController: NSViewController { 13 | 14 | // MARK: - Private properties 15 | // MARK: IBOutlets 16 | 17 | @IBOutlet private weak var updateVersionTextField: NSTextField! 18 | @IBOutlet private weak var updateStateTextField: NSTextField! 19 | @IBOutlet private weak var metaTextField: NSTextField! 20 | 21 | @IBOutlet private weak var requiredVersionTextField: NSTextField! 22 | @IBOutlet private weak var lastVersionAvailableTextField: NSTextField! 23 | @IBOutlet private weak var installedVersionTextField: NSTextField! 24 | @IBOutlet private weak var notificationTypeTextField: NSTextField! 25 | @IBOutlet private weak var requirementsTextField: NSTextField! 26 | 27 | // MARK: - View Lifecycle 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | // Do view setup here. 32 | checkAppVersion() 33 | checkAppStoreVersion() 34 | } 35 | } 36 | 37 | // MARK: - Private methods - 38 | 39 | private extension ConfigurationController { 40 | 41 | func checkAppVersion() { 42 | 43 | let options = PoVRequestOptions() 44 | 45 | options.addRequirement(key: "region", ofType: String.self) { $0.starts(with: "hr") } 46 | 47 | options.addRequirement(key: "bluetooth", ofType: String.self) { $0.starts(with: "5") } 48 | 49 | 50 | let princeOfVersionsURL = URL(string: Constants.princeOfVersionsURL)! 51 | 52 | PrinceOfVersions.checkForUpdates( 53 | from: princeOfVersionsURL, 54 | options: options, 55 | completion: { [weak self] response in 56 | switch response.result { 57 | case .success(let updateResultData): 58 | self?.fillUI(with: updateResultData) 59 | case .failure: 60 | // Handle error 61 | break 62 | } 63 | } 64 | ) 65 | 66 | } 67 | 68 | // In sample app, error will occur as bundle ID 69 | // of the app is not available on the App Store 70 | 71 | func checkAppStoreVersion() { 72 | // In sample app, error will occur as bundle ID 73 | // of the app is not available on the App Store 74 | PrinceOfVersions.checkForUpdateFromAppStore( 75 | trackPhaseRelease: false, 76 | completion: { result in 77 | switch result { 78 | case .success: 79 | // Handle success 80 | break 81 | case .failure: 82 | // Handle error 83 | break 84 | } 85 | }) 86 | } 87 | } 88 | 89 | private extension ConfigurationController { 90 | 91 | func fillUI(with infoResponse: UpdateResult) { 92 | fillUpdateResultUI(with: infoResponse) 93 | fillVersionInfoUI(with: infoResponse.updateInfo) 94 | } 95 | 96 | func fillUpdateResultUI(with infoResponse: UpdateResult) { 97 | updateVersionTextField.stringValue = infoResponse.updateVersion.description 98 | updateStateTextField.stringValue = infoResponse.updateState.updateState 99 | metaTextField.stringValue = "\(infoResponse.metadata ?? [:])" 100 | } 101 | 102 | func fillVersionInfoUI(with versionInfo: UpdateInfo) { 103 | requiredVersionTextField.stringValue = versionInfo.requiredVersion?.description ?? "" 104 | lastVersionAvailableTextField.stringValue = versionInfo.lastVersionAvailable?.description ?? "" 105 | installedVersionTextField.stringValue = versionInfo.installedVersion.description 106 | notificationTypeTextField.stringValue = versionInfo.notificationType == .once ? "ONCE" : "ALWAYS" 107 | requirementsTextField.stringValue = "\(versionInfo.requirements ?? [:])" 108 | } 109 | } 110 | 111 | private extension UpdateStatus { 112 | 113 | var updateState: String { 114 | switch self { 115 | case .noUpdateAvailable: return "No Update Available" 116 | case .requiredUpdateNeeded: return "Required Update Needed" 117 | case .newUpdateAvailable: return "New Update Available" 118 | default: return "" 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/osx,xcode,objective-c,swift 2 | 3 | ### OSX ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | 32 | ### Xcode ### 33 | # Xcode 34 | # 35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 36 | 37 | ## Build generated 38 | build/ 39 | DerivedData/ 40 | 41 | ## Various settings 42 | *.pbxuser 43 | !default.pbxuser 44 | *.mode1v3 45 | !default.mode1v3 46 | *.mode2v3 47 | !default.mode2v3 48 | *.perspectivev3 49 | !default.perspectivev3 50 | xcuserdata/ 51 | 52 | ## Other 53 | *.moved-aside 54 | *.xccheckout 55 | *.xcscmblueprint 56 | 57 | 58 | ### Objective-C ### 59 | # Xcode 60 | # 61 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 62 | 63 | ## Build generated 64 | build/ 65 | DerivedData/ 66 | 67 | ## Various settings 68 | *.pbxuser 69 | !default.pbxuser 70 | *.mode1v3 71 | !default.mode1v3 72 | *.mode2v3 73 | !default.mode2v3 74 | *.perspectivev3 75 | !default.perspectivev3 76 | xcuserdata/ 77 | 78 | ## Other 79 | *.moved-aside 80 | *.xcuserstate 81 | 82 | ## Obj-C/Swift specific 83 | *.hmap 84 | *.ipa 85 | *.dSYM.zip 86 | *.dSYM 87 | 88 | # CocoaPods 89 | # 90 | # We recommend against adding the Pods directory to your .gitignore. However 91 | # you should judge for yourself, the pros and cons are mentioned at: 92 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 93 | # 94 | # Pods/ 95 | 96 | # Carthage 97 | # 98 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 99 | # Carthage/Checkouts 100 | 101 | Carthage/Build 102 | 103 | # fastlane 104 | # 105 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 106 | # screenshots whenever they are needed. 107 | # For more information about the recommended setup visit: 108 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 109 | 110 | fastlane/report.xml 111 | fastlane/screenshots 112 | 113 | #Code Injection 114 | # 115 | # After new code Injection tools there's a generated folder /iOSInjectionProject 116 | # https://github.com/johnno1962/injectionforxcode 117 | 118 | iOSInjectionProject/ 119 | 120 | ### Objective-C Patch ### 121 | *.xcscmblueprint 122 | 123 | 124 | ### Swift ### 125 | # Xcode 126 | # 127 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 128 | 129 | ## Build generated 130 | build/ 131 | DerivedData/ 132 | 133 | ## Various settings 134 | *.pbxuser 135 | !default.pbxuser 136 | *.mode1v3 137 | !default.mode1v3 138 | *.mode2v3 139 | !default.mode2v3 140 | *.perspectivev3 141 | !default.perspectivev3 142 | xcuserdata/ 143 | 144 | ## Other 145 | *.moved-aside 146 | *.xcuserstate 147 | 148 | ## Obj-C/Swift specific 149 | *.hmap 150 | *.ipa 151 | *.dSYM.zip 152 | *.dSYM 153 | 154 | ## Playgrounds 155 | timeline.xctimeline 156 | playground.xcworkspace 157 | 158 | # Swift Package Manager 159 | # 160 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 161 | # Packages/ 162 | .build/ 163 | 164 | # CocoaPods 165 | # 166 | # We recommend against adding the Pods directory to your .gitignore. However 167 | # you should judge for yourself, the pros and cons are mentioned at: 168 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 169 | # 170 | # Pods/ 171 | 172 | # Carthage 173 | # 174 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 175 | # Carthage/Checkouts 176 | 177 | Carthage/Build 178 | 179 | # fastlane 180 | # 181 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 182 | # screenshots whenever they are needed. 183 | # For more information about the recommended setup visit: 184 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 185 | 186 | fastlane/report.xml 187 | fastlane/Preview.html 188 | fastlane/screenshots 189 | fastlane/test_output 190 | -------------------------------------------------------------------------------- /PrinceOfVersionsSample/PrinceOfVersionsIosSample/Supporting Files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/PrinceOfVersionsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrinceOfVersionsTest.swift 3 | // Prince of versions 4 | // 5 | // Created by Jasmin Abou Aldan on 21/09/2016. 6 | // Copyright © 2016 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PrinceOfVersions 11 | 12 | class PrinceOfVersionsTest: XCTestCase { 13 | 14 | static let testURL = URL(string: "https://pastebin.com/raw/0MfYmWGu")! 15 | 16 | func testLoadConfigurationBaseOnMain() { 17 | runAsyncTest { finished in 18 | PrinceOfVersions.checkForUpdates( 19 | from: PrinceOfVersionsTest.testURL, 20 | completion: { _ in 21 | XCTAssertTrue(Thread.isMainThread) 22 | finished() 23 | }) 24 | } 25 | } 26 | 27 | func testLoadConfigurationBaseOnBackground() { 28 | runAsyncTest { finished in 29 | PrinceOfVersions.checkForUpdates( 30 | from: PrinceOfVersionsTest.testURL, 31 | callbackQueue: .global(qos: .background), 32 | completion: { _ in 33 | XCTAssertFalse(Thread.isMainThread) 34 | finished() 35 | }) 36 | } 37 | } 38 | 39 | func testAutomaticUpdateFromStore() { 40 | 41 | let bundle = Bundle(for: type(of: self)) 42 | let jsonPath = bundle.path(forResource: "app_store_version_example", ofType: "json")! 43 | 44 | let installedVersion = try! Version(string: "1.0.0-1") 45 | let lastVersionAvailable = try! Version(string: "0.1.0") 46 | let minimumSdkForLatestVersion = try! Version(string: "12.0") 47 | 48 | runAsyncTest { finished in 49 | PrinceOfVersions.internalyGetDataFromAppStore( 50 | URL(fileURLWithPath: jsonPath), 51 | trackPhaseRelease: false, 52 | bundle: bundle, 53 | testMode: true, 54 | cachePolicy: .reloadIgnoringLocalCacheData, 55 | completion: { result in 56 | switch result { 57 | case .success(let updateResult): 58 | XCTAssert(updateResult.updateInfo.installedVersion == installedVersion) 59 | 60 | guard let latestVersion = updateResult.updateInfo.lastVersionAvailable else { 61 | XCTFail("Last version available should not be nil") 62 | return 63 | } 64 | 65 | XCTAssert(latestVersion == lastVersionAvailable) 66 | if let minSdkForLatestVersion = updateResult.updateInfo.configurationData?.minimumOsVersion { 67 | XCTAssert(minSdkForLatestVersion == minimumSdkForLatestVersion) 68 | } else { 69 | XCTFail("min sdk should not be nil") 70 | } 71 | XCTAssert(!updateResult.phaseReleaseInProgress) 72 | finished() 73 | case .failure: 74 | XCTFail("Invalid data") 75 | finished() 76 | } 77 | } 78 | ) 79 | } 80 | } 81 | 82 | func testAutomaticUpdateFromStorePhased() { 83 | let bundle = Bundle(for: type(of: self)) 84 | let jsonPath = bundle.path(forResource: "app_store_version_example", ofType: "json")! 85 | runAsyncTest { finished in 86 | PrinceOfVersions.internalyGetDataFromAppStore( 87 | URL(fileURLWithPath: jsonPath), 88 | trackPhaseRelease: true, 89 | bundle: bundle, 90 | testMode: true, 91 | cachePolicy: .reloadIgnoringLocalCacheData, 92 | completion: { result in 93 | switch result { 94 | case .success(let info): 95 | XCTAssert(!info.phaseReleaseInProgress) 96 | finished() 97 | case .failure: 98 | XCTFail("Invalid data") 99 | finished() 100 | } 101 | } 102 | ) 103 | } 104 | } 105 | } 106 | 107 | private extension PrinceOfVersionsTest { 108 | 109 | func runAsyncTest(with description: String = #function, test: ( @escaping () -> Void ) -> Void) { 110 | let expectation = XCTestExpectation(description: description) 111 | test { 112 | expectation.fulfill() 113 | } 114 | wait(for: [expectation], timeout: 5.0) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/PoVDataTypes/Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Filip Beć on 10/10/16. 6 | // Copyrhs © 2016 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum VersionError: Error { 12 | case invalidString 13 | case invalidMajorVersion 14 | } 15 | 16 | public class Version: NSObject, Codable { 17 | @objc public let major: Int 18 | @objc public let minor: Int 19 | @objc public let patch: Int 20 | @objc public var build: Int = 0 21 | 22 | public var wasNotified: Bool { 23 | return UserDefaults.standard.bool(forKey: versionUserDefaultKey) 24 | } 25 | 26 | private var versionUserDefaultKey: String { 27 | return "co.infinum.prince-of-versions.version-" + self.description 28 | } 29 | 30 | @objc public func markNotified() { 31 | UserDefaults.standard.set(true, forKey: versionUserDefaultKey) 32 | } 33 | 34 | required public convenience init(from decoder: Decoder) throws { 35 | let string = try decoder.singleValueContainer().decode(String.self) 36 | try self.init(string: string) 37 | } 38 | 39 | init(string: String) throws { 40 | 41 | let versionBuildComponents = string.components(separatedBy: "-") 42 | guard let versionComponents = versionBuildComponents.first?.components(separatedBy: ".") else { 43 | throw VersionError.invalidString 44 | } 45 | guard !versionComponents.isEmpty else { 46 | throw VersionError.invalidString 47 | } 48 | 49 | if versionBuildComponents.count > 1 { 50 | build = Version.number(from: versionBuildComponents, atIndex: 1) ?? 0 51 | } 52 | 53 | if let majorVersion = Version.number(from: versionComponents, atIndex: 0) { 54 | major = majorVersion 55 | } else { 56 | throw VersionError.invalidMajorVersion 57 | } 58 | 59 | minor = Version.number(from: versionComponents, atIndex: 1) ?? 0 60 | patch = Version.number(from: versionComponents, atIndex: 2) ?? 0 61 | } 62 | 63 | #if os(macOS) 64 | init(macVersion: OperatingSystemVersion) { 65 | major = macVersion.majorVersion 66 | minor = macVersion.minorVersion 67 | patch = macVersion.patchVersion 68 | } 69 | #endif 70 | 71 | private static func number(from components: [String], atIndex index: Int) -> Int? { 72 | guard components.indices.contains(index) else { 73 | return nil 74 | } 75 | return Int(components[index]) 76 | } 77 | 78 | @objc override public var description: String { 79 | return "\(major).\(minor).\(patch)-\(build)" 80 | } 81 | } 82 | 83 | // MARK: - Comparison - 84 | 85 | extension Version { 86 | 87 | @objc(isGreaterThanVersion:) 88 | public func isGreaterThan(_ version: Version) -> Bool { 89 | return self > version 90 | } 91 | 92 | @objc(isGreaterOrEqualToVersion:) 93 | public func isGreaterOrEqualTo(_ version: Version) -> Bool { 94 | return self >= version 95 | } 96 | 97 | @objc(isLowerOrEqualToVersion:) 98 | public func isLowerOrEqualTo(_ version: Version) -> Bool { 99 | return self <= version 100 | } 101 | 102 | @objc(isEqualToVersion:) 103 | public func isEqualTo(_ version: Version) -> Bool { 104 | return self == version 105 | } 106 | 107 | @objc(isNotEqualToVersion:) 108 | public func isNotEqualTo(_ version: Version) -> Bool { 109 | return self != version 110 | } 111 | 112 | public static func max(_ version1: Version, _ version2: Version) -> Version { 113 | return version1.isGreaterThan(version2) ? version1 : version2 114 | } 115 | } 116 | 117 | extension Version: Comparable { 118 | 119 | private var tuple: (Int, Int, Int, Int) { 120 | return (major, minor, patch, build) 121 | } 122 | 123 | public static func == (lhs: Version, rhs: Version) -> Bool { 124 | return lhs.tuple == rhs.tuple 125 | } 126 | 127 | static func != (lhs: Version, rhs: Version) -> Bool { 128 | return !(lhs == rhs) 129 | } 130 | 131 | public static func < (lhs: Version, rhs: Version) -> Bool { 132 | return lhs.tuple < rhs.tuple 133 | } 134 | 135 | public static func <= (lhs: Version, rhs: Version) -> Bool { 136 | return lhs.tuple <= rhs.tuple 137 | } 138 | 139 | public static func > (lhs: Version, rhs: Version) -> Bool { 140 | return lhs.tuple > rhs.tuple 141 | } 142 | 143 | public static func >= (lhs: Version, rhs: Version) -> Bool { 144 | return lhs.tuple >= rhs.tuple 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/UpdateInfoTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateInfoTest.swift 3 | // Prince of versions 4 | // 5 | // Created by Jasmin Abou Aldan on 21/09/2016. 6 | // Copyright © 2016 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PrinceOfVersions 11 | 12 | class UpdateInfoTest: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testCheckingValidContent() { 25 | let bundle = Bundle(for: type(of: self)) 26 | 27 | var info: UpdateInfo? 28 | if let jsonPath = bundle.path(forResource: "valid_update_full", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 29 | do { 30 | let decoder = JSONDecoder() 31 | decoder.keyDecodingStrategy = .convertFromSnakeCase 32 | info = try decoder.decode(UpdateInfo.self, from: data) 33 | } catch let error { 34 | XCTFail(error.localizedDescription) 35 | } 36 | } 37 | 38 | guard let updateInfo = info else { 39 | XCTFail("Update info should not be nil") 40 | return 41 | } 42 | 43 | let updateResult = UpdateResult(updateInfo: updateInfo) 44 | 45 | XCTAssertNotNil(updateResult.updateInfo.requiredVersion, "Value for required version should not be nil") 46 | } 47 | 48 | func testCheckingValidV2Content() { 49 | 50 | let bundle = Bundle(for: type(of: self)) 51 | 52 | var info: UpdateInfo? 53 | 54 | if let jsonPath = bundle.path(forResource: "valid_update_only_v2_metadata_empty", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 55 | do { 56 | let decoder = JSONDecoder() 57 | decoder.keyDecodingStrategy = .convertFromSnakeCase 58 | info = try decoder.decode(UpdateInfo.self, from: data) 59 | 60 | } catch let error { 61 | XCTFail(error.localizedDescription) 62 | } 63 | } 64 | 65 | guard let updateInfo = info else { 66 | XCTFail("Update info should not be nil") 67 | return 68 | } 69 | 70 | let updateResult = UpdateResult(updateInfo: updateInfo) 71 | 72 | XCTAssertNotNil(updateResult.updateInfo.requiredVersion, "Value for required version should not be nil") 73 | } 74 | 75 | func testCheckingValidV2OnlyIosContent() { 76 | 77 | let bundle = Bundle(for: type(of: self)) 78 | 79 | var info: UpdateInfo? 80 | 81 | if let jsonPath = bundle.path(forResource: "valid_update_only_v2_ios", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 82 | do { 83 | let decoder = JSONDecoder() 84 | decoder.keyDecodingStrategy = .convertFromSnakeCase 85 | info = try decoder.decode(UpdateInfo.self, from: data) 86 | 87 | } catch let error { 88 | XCTFail(error.localizedDescription) 89 | } 90 | } 91 | 92 | guard let updateInfo = info else { 93 | XCTFail("Update info should not be nil") 94 | return 95 | } 96 | 97 | let updateResult = UpdateResult(updateInfo: updateInfo) 98 | 99 | #if os(iOS) 100 | XCTAssertNotNil(updateResult.updateInfo.requiredVersion, "Value for required version should not be nil") 101 | #endif 102 | } 103 | 104 | func testCheckingValidV2OnlyMacosContent() { 105 | 106 | let bundle = Bundle(for: type(of: self)) 107 | 108 | var info: UpdateInfo? 109 | 110 | if let jsonPath = bundle.path(forResource: "valid_update_only_v2_macos", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 111 | do { 112 | let decoder = JSONDecoder() 113 | decoder.keyDecodingStrategy = .convertFromSnakeCase 114 | info = try decoder.decode(UpdateInfo.self, from: data) 115 | 116 | } catch let error { 117 | XCTFail(error.localizedDescription) 118 | } 119 | } 120 | 121 | guard let updateInfo = info else { 122 | XCTFail("Update info should not be nil") 123 | return 124 | } 125 | 126 | let updateResult = UpdateResult(updateInfo: updateInfo) 127 | 128 | #if os(macOS) 129 | XCTAssertNotNil(updateResult.updateInfo.requiredVersion, "Value for required version should not be nil") 130 | #endif 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/Objective-C Extensions/CheckUpdatesObjectiveCExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckUpdatesObjectiveCExtensions.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 05/02/2020. 6 | // Copyright © 2020 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Check updates - 12 | 13 | extension PrinceOfVersions { 14 | 15 | public typealias ObjectCompletionBlock = (__ObjCUpdateResponse) -> Void 16 | public typealias ObjectErrorBlock = (NSError) -> Void 17 | 18 | /** 19 | Used for getting the versioning configuration stored on server. Uses URL for data fetch. 20 | 21 | After check with server is finished, this method will return all informations about the app versioning. 22 | It's up to the user to handle that info in a way sutable for the app. 23 | 24 | - Parameters: 25 | * URL: URL that containts configuration data. 26 | * completion: The completion handler to call when the load request is complete. It returns result that contains UpdateResult data 27 | * error: The completion handler to call when load request errors 28 | 29 | - returns: Discardable `URLSessionDataTask` 30 | */ 31 | @available(swift, obsoleted: 1.0) 32 | @objc(checkForUpdatesFromURL:completion:error:) 33 | @discardableResult 34 | public static func checkForUpdatesFromURL(_ URL: URL, completion: @escaping ObjectCompletionBlock, error: @escaping ObjectErrorBlock) -> URLSessionDataTask? { 35 | return internalyLoadAndPrepareConfiguration(from: URL, callbackQueue: .main, options: PoVRequestOptions(), completion: completion, error: error) 36 | } 37 | 38 | /** 39 | Used for getting the versioning configuration stored on server. Uses URL for data fetch with posibility to set custom http headers and certificate pinning enabling. 40 | 41 | After check with server is finished, this method will return all informations about the app versioning. 42 | It's up to the user to handle that info in a way sutable for the app. 43 | 44 | - Parameters: 45 | * URL: URL that containts configuration data. 46 | * options: Used for additional configuration such as `shouldPinCertificates`, `httpHeaderFields` and `userRequirements` 47 | * completion: The completion handler to call when the load request is complete. It returns result that contains UpdateResult data 48 | * error: The completion handler to call when load request errors 49 | 50 | - returns: Discardable `URLSessionDataTask` 51 | */ 52 | @available(swift, obsoleted: 1.0) 53 | @objc(checkForUpdatesFromURL:options:completion:error:) 54 | @discardableResult 55 | public static func checkForUpdatesFromURL(_ URL: URL, options: PoVRequestOptions, completion: @escaping ObjectCompletionBlock, error: @escaping ObjectErrorBlock) -> URLSessionDataTask? { 56 | return internalyLoadAndPrepareConfiguration(from: URL, callbackQueue: .main, options: options, completion: completion, error: error) 57 | } 58 | 59 | /** 60 | Used for getting the versioning configuration stored on server. Uses URL for data fetch with posibility to set custom callback queue. 61 | 62 | After check with server is finished, this method will return all informations about the app versioning. 63 | It's up to the user to handle that info in a way sutable for the app. 64 | 65 | - Parameters: 66 | * URL: URL that containts configuration data. 67 | * callbackQueue: The queue on which the completion handler is dispatched. By default, `main` queue is used. 68 | * completion: The completion handler to call when the load request is complete. It returns result that contains UpdateResult data 69 | * error: The completion handler to call when load request errors 70 | 71 | - returns: Discardable `URLSessionDataTask` 72 | */ 73 | @available(swift, obsoleted: 1.0) 74 | @objc(checkForUpdatesFromURL:callbackQueue:completion:error:) 75 | @discardableResult 76 | public static func checkForUpdatesFromURL(_ URL: URL, callbackQueue: DispatchQueue, completion: @escaping ObjectCompletionBlock, error: @escaping ObjectErrorBlock) -> URLSessionDataTask? { 77 | return internalyLoadAndPrepareConfiguration(from: URL, callbackQueue: callbackQueue, options: PoVRequestOptions(), completion: completion, error: error) 78 | } 79 | 80 | /** 81 | Used for getting the versioning configuration stored on server. Uses URL for data fetch with posibility to set custom http headers, certificate pinning enabling and custom callback queue. 82 | 83 | After check with server is finished, this method will return all informations about the app versioning. 84 | It's up to the user to handle that info in a way sutable for the app. 85 | 86 | - Parameters: 87 | * URL: URL that containts configuration data. 88 | * callbackQueue: The queue on which the completion handler is dispatched. By default, `main` queue is used. 89 | * options: Used for additional configuration such as `shouldPinCertificates`, `httpHeaderFields` and `userRequirements` 90 | * completion: The completion handler to call when the load request is complete. It returns result that contains UpdateResult data 91 | * error: The completion handler to call when load request errors 92 | 93 | - returns: Discardable `URLSessionDataTask` 94 | */ 95 | @available(swift, obsoleted: 1.0) 96 | @objc(checkForUpdatesFromURL:callbackQueue:options:completion:error:) 97 | @discardableResult 98 | public static func checkForUpdatesFromURL(_ URL: URL, callbackQueue: DispatchQueue, options: PoVRequestOptions, completion: @escaping ObjectCompletionBlock, error: @escaping ObjectErrorBlock) -> URLSessionDataTask? { 99 | return internalyLoadAndPrepareConfiguration(from: URL, callbackQueue: callbackQueue, options: options, completion: completion, error: error) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/ResponseModels/UpdateData/UpdateInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateInfo.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 06/07/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | #if canImport(UIKit) 12 | import UIKit 13 | #elseif canImport(AppKit) 14 | import AppKit 15 | #endif 16 | 17 | // MARK: - Internal configuration data - 18 | 19 | public struct UpdateInfo: Decodable { 20 | 21 | // MARK: - Private properties - 22 | 23 | private var ios: [ConfigurationData]? 24 | // used only when supporting both PoV versions < 4.0, and versions >= 4.0 25 | // older version configuration is stored in ios property, and newer in ios2 26 | private var ios2: [ConfigurationData]? 27 | private var macos: [ConfigurationData]? 28 | // used only when supporting both PoV versions < 4.0, and versions >= 4.0 29 | // older version configuration is stored in macos property, and newer in macos2 30 | private var macos2: [ConfigurationData]? 31 | private var meta: [String: AnyDecodable]? 32 | 33 | // MARK: - Internal properties - 34 | 35 | internal var bundle = Bundle.main 36 | 37 | internal var configurations: [ConfigurationData]? { 38 | #if os(iOS) 39 | return ios ?? ios2 40 | #elseif os(macOS) 41 | return macos ?? macos2 42 | #endif 43 | } 44 | 45 | internal var configuration: ConfigurationData? 46 | 47 | internal var sdkVersion: Version? { 48 | #if os(iOS) 49 | return try? Version(string: UIDevice.current.systemVersion) 50 | #elseif os(macOS) 51 | return Version(macVersion: ProcessInfo.processInfo.operatingSystemVersion) 52 | #endif 53 | } 54 | 55 | internal var currentInstalledVersion: Version? { 56 | 57 | guard 58 | let currentVersionString = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, 59 | let currentBuildNumberString = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String 60 | else { 61 | return nil 62 | } 63 | 64 | return try? Version(string: currentVersionString + "-" + currentBuildNumberString) 65 | } 66 | 67 | internal var metadata: [String: Any]? { 68 | 69 | if meta == nil && configuration?.meta == nil { return nil } 70 | 71 | let globalMeta = meta ?? [:] 72 | let configMeta = configuration?.meta ?? [:] 73 | 74 | return globalMeta 75 | .merging(configMeta, uniquingKeysWith: { (_, newValue) in newValue }) 76 | .mapValues { $0.value } 77 | } 78 | 79 | internal var userRequirements: [String: ((Any) -> Bool)] = [:] { 80 | didSet { 81 | if oldValue.keys == userRequirements.keys && configuration != nil { return } 82 | configuration = configurations?.first { meetsUserRequirements($0.requirements) } 83 | } 84 | } 85 | 86 | // MARK: - Init - 87 | 88 | public init(from decoder: Decoder) throws { 89 | let container = try decoder.container(keyedBy: CodingKeys.self) 90 | 91 | ios = container.decodeConfiguration(.ios) 92 | ios2 = container.decodeConfiguration(.ios2) 93 | macos = container.decodeConfiguration(.macos) 94 | macos2 = container.decodeConfiguration(.macos2) 95 | meta = container.decodeMeta(.meta) 96 | 97 | defer { 98 | userRequirements = [:] 99 | } 100 | } 101 | 102 | // MARK: - Coding keys - 103 | 104 | enum CodingKeys: String, CodingKey { 105 | case ios, ios2, macos, macos2, meta 106 | } 107 | } 108 | 109 | // MARK: - Private methods - 110 | 111 | private extension UpdateInfo { 112 | 113 | var requiredOSVersionCheck: ((Any) -> Bool) { 114 | return { requiredOSVersion -> Bool in 115 | guard 116 | let installedOSVersion = self.sdkVersion, 117 | let requiredOSVersion = requiredOSVersion as? Version 118 | else { return true } 119 | return installedOSVersion >= requiredOSVersion 120 | } 121 | } 122 | 123 | func meetsUserRequirements(_ requirements: Requirements?) -> Bool { 124 | 125 | guard let requirements = requirements else { return true } 126 | 127 | var requirementChecks = userRequirements 128 | if requirements.requiredOSVersion != nil { 129 | requirementChecks.updateValue(requiredOSVersionCheck, forKey: "requiredOsVersion") 130 | } 131 | 132 | return requirements.allRequirements?.allSatisfy { 133 | guard let checkRequirement = requirementChecks[$0.key] else { return false } 134 | return checkRequirement($0.value) 135 | } ?? true 136 | } 137 | } 138 | 139 | // MARK: - Public properties - 140 | 141 | extension UpdateInfo: BaseUpdateInfo { 142 | 143 | /// Returns latest available version of the app. 144 | public var lastVersionAvailable: Version? { 145 | return configuration?.lastVersionAvailable 146 | } 147 | 148 | /// Returns installed version of the app. 149 | public var installedVersion: Version { 150 | guard let version = currentInstalledVersion else { 151 | preconditionFailure("Unable to get installed version data") 152 | } 153 | return version 154 | } 155 | } 156 | 157 | extension UpdateInfo { 158 | 159 | /// Returns minimum required version of the app. 160 | public var requiredVersion: Version? { 161 | return configuration?.requiredVersion 162 | } 163 | 164 | /// Returns requirements for configuration. 165 | public var requirements: [String : Any]? { 166 | return configuration?.requirements?.allRequirements 167 | } 168 | 169 | /// Returns notification frequency for configuration. 170 | public var notificationType: NotificationType { 171 | return configuration?.notifyLastVersionFrequency ?? .once 172 | } 173 | } 174 | 175 | // MARK: - Private helpers - 176 | 177 | private extension KeyedDecodingContainer { 178 | 179 | func decodeConfiguration(_ key: K) -> [ConfigurationData]? { 180 | return try? decode([ConfigurationData].self, forKey: key) 181 | } 182 | 183 | func decodeMeta(_ key: K) -> [String: AnyDecodable]? { 184 | return try? decode([String: AnyDecodable].self, forKey: key) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/RequirementsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequirementsTest.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Ivana Mršić on 05/06/2020. 6 | // 7 | 8 | import XCTest 9 | @testable import PrinceOfVersions 10 | 11 | class RequirementsTest: XCTestCase { 12 | 13 | var updateInfoWithoutRequirements: UpdateInfo? 14 | var updateInfoWithIncreasingRequirements: UpdateInfo? 15 | var updateInfoWithDecreasingRequirements: UpdateInfo? 16 | var updateInfoWithShuffledRequirements: UpdateInfo? 17 | 18 | let regionKey = "region" 19 | let bluetoothKey = "bluetooth" 20 | 21 | override func setUp() { 22 | super.setUp() 23 | fetchData() 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testCheckingValidWithoutRequirementsWithoutChecks() { 32 | XCTAssertNotNil(updateInfoWithoutRequirements?.configuration) 33 | } 34 | 35 | func testCheckingValidWithoutRequirementsWithChecks() { 36 | updateInfoWithoutRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 37 | XCTAssertNotNil(updateInfoWithoutRequirements?.configuration) 38 | } 39 | 40 | func testCheckingValidWithIncreasingRequirementsWithoutChecks() { 41 | XCTAssertNotNil(updateInfoWithIncreasingRequirements?.configuration) 42 | } 43 | 44 | func testCheckingValidWithIncreasingRequirementsWithOneCheck() { 45 | updateInfoWithIncreasingRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 46 | XCTAssertNotNil(updateInfoWithIncreasingRequirements?.configuration) 47 | } 48 | 49 | func testCheckingValidWithIncreasingRequirementsWithTwoChecks() { 50 | 51 | updateInfoWithIncreasingRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 52 | 53 | updateInfoWithIncreasingRequirements?.userRequirements.updateValue(bluetoothCheck, forKey: bluetoothKey) 54 | 55 | let configsAreEqual = configsEqual( 56 | config1: updateInfoWithIncreasingRequirements?.configurations?.first, 57 | config2: updateInfoWithIncreasingRequirements?.configuration 58 | ) 59 | 60 | XCTAssertTrue(configsAreEqual) 61 | } 62 | 63 | func testCheckingValidWithDecreasingRequirementsWithoutChecks() { 64 | XCTAssertNotNil(updateInfoWithDecreasingRequirements?.configuration) 65 | } 66 | 67 | func testCheckingValidWithDecreasingRequirementsWithOneCheck() { 68 | 69 | updateInfoWithDecreasingRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 70 | 71 | let configsAreEqual = configsEqual(config1: updateInfoWithDecreasingRequirements?.configurations?.first, config2: updateInfoWithDecreasingRequirements?.configuration) 72 | 73 | XCTAssertFalse(configsAreEqual) 74 | } 75 | 76 | func testCheckingValidWithDecreasingRequirementsWithTwoChecks() { 77 | 78 | updateInfoWithDecreasingRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 79 | 80 | updateInfoWithDecreasingRequirements?.userRequirements.updateValue(bluetoothCheck, forKey: bluetoothKey) 81 | 82 | let configsAreEqual = configsEqual( 83 | config1: updateInfoWithDecreasingRequirements?.configuration, 84 | config2: updateInfoWithDecreasingRequirements?.configurations?.first 85 | ) 86 | 87 | XCTAssertFalse(configsAreEqual) 88 | } 89 | 90 | func testCheckingValidWithShuffledRequirementsWithoutChecks() { 91 | XCTAssertNotNil(updateInfoWithShuffledRequirements?.configuration) 92 | } 93 | 94 | func testCheckingValidWithShuffledRequirementsWithOneCheck() { 95 | 96 | updateInfoWithShuffledRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 97 | 98 | let configsAreEqual = configsEqual(config1: updateInfoWithShuffledRequirements?.configurations?.first, config2: updateInfoWithShuffledRequirements?.configuration) 99 | 100 | XCTAssertTrue(configsAreEqual) 101 | } 102 | 103 | func testCheckingValidWithShuffledRequirementsWithTwoChecks() { 104 | 105 | updateInfoWithShuffledRequirements?.userRequirements.updateValue(regionCheck, forKey: regionKey) 106 | 107 | updateInfoWithShuffledRequirements?.userRequirements.updateValue(bluetoothCheck, forKey: bluetoothKey) 108 | 109 | let configsAreEqual = configsEqual( 110 | config1: updateInfoWithShuffledRequirements?.configuration, 111 | config2: updateInfoWithShuffledRequirements?.configurations?.first 112 | ) 113 | 114 | XCTAssertTrue(configsAreEqual) 115 | } 116 | } 117 | 118 | // MARK: - Requirement Check Closures - 119 | 120 | extension RequirementsTest { 121 | 122 | var bluetoothCheck: (Any) -> Bool { 123 | return { value in 124 | guard let value = value as? String else { return false } 125 | return value.starts(with: "5") 126 | } 127 | } 128 | 129 | var regionCheck: (Any) -> Bool { 130 | return { region in 131 | guard let region = region as? String else { return false } 132 | return region.starts(with: "hr") 133 | } 134 | } 135 | } 136 | 137 | // MARK: - Helpers - 138 | 139 | private extension RequirementsTest { 140 | 141 | func fetchData() { 142 | 143 | let bundle = Bundle(for: type(of: self)) 144 | 145 | if let jsonPath = bundle.path(forResource: "valid_update_no_requirements", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 146 | do { 147 | let decoder = JSONDecoder() 148 | decoder.keyDecodingStrategy = .convertFromSnakeCase 149 | updateInfoWithoutRequirements = try decoder.decode(UpdateInfo.self, from: data) 150 | } catch let error { 151 | XCTFail(error.localizedDescription) 152 | } 153 | } 154 | 155 | if let jsonPath = bundle.path(forResource: "valid_update_with_increasing_requirements", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 156 | do { 157 | let decoder = JSONDecoder() 158 | decoder.keyDecodingStrategy = .convertFromSnakeCase 159 | updateInfoWithIncreasingRequirements = try decoder.decode(UpdateInfo.self, from: data) 160 | } catch let error { 161 | XCTFail(error.localizedDescription) 162 | } 163 | } 164 | 165 | if let jsonPath = bundle.path(forResource: "valid_update_with_decreasing_requirements", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 166 | do { 167 | let decoder = JSONDecoder() 168 | decoder.keyDecodingStrategy = .convertFromSnakeCase 169 | updateInfoWithDecreasingRequirements = try decoder.decode(UpdateInfo.self, from: data) 170 | } catch let error { 171 | XCTFail(error.localizedDescription) 172 | } 173 | } 174 | 175 | if let jsonPath = bundle.path(forResource: "valid_update_with_shuffled_requirements", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) { 176 | do { 177 | let decoder = JSONDecoder() 178 | decoder.keyDecodingStrategy = .convertFromSnakeCase 179 | updateInfoWithShuffledRequirements = try decoder.decode(UpdateInfo.self, from: data) 180 | } catch let error { 181 | XCTFail(error.localizedDescription) 182 | } 183 | } 184 | } 185 | 186 | func configsEqual(config1: ConfigurationData?, config2: ConfigurationData?) -> Bool { 187 | return 188 | config1?.lastVersionAvailable == config2?.lastVersionAvailable && 189 | config1?.notifyLastVersionFrequency == config2?.notifyLastVersionFrequency && 190 | config1?.requiredVersion == config2?.requiredVersion && 191 | config1?.requirements?.allRequirements?.count == config2?.requirements?.allRequirements?.count 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Sources/PrinceOfVersions/Objective-C Helpers/Objective-C Extensions/CheckUpdateFromAppStoreObjectiveCExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckUpdatesObjectiveCExtensions.swift 3 | // PrinceOfVersions 4 | // 5 | // Created by Jasmin Abou Aldan on 05/02/2020. 6 | // Copyright © 2020 Infinum Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Check updates - 12 | 13 | extension PrinceOfVersions { 14 | 15 | public typealias AppStoreObjectCompletionBlock = (__ObjCAppStoreResult) -> Void 16 | 17 | /** 18 | Used for getting the versioning information from the AppStore Connect. 19 | 20 | After check with server is finished, this method will return all informations about the app versioning available on the AppStore Connect. 21 | It's up to the user to handle that info in a way sutable for the app. 22 | 23 | Update status can only take on value `UpdateStatus.noUpdateAvailable` or `UpdateStatus.newUpdateAvailable`, but it can't be `UpdateStatus.requiredUpdateNeeded` since there is no way to determine if the update version is mandatory with this check. 24 | 25 | Flag `trackPhaseRelese` will be set to `YES`. 26 | If flag `trackPhaseRelease` is set to `NO`, the value of the `phaseReleaseInProgress` will instantly be `NO` as phased release is not used. 27 | Otherwise, if we have to check `trackPhaseRelease`, value of `phaseReleaseInProgress` will return `NO` once phased release period of 7 days is over. 28 | 29 | __WARNING:__ As we are not able to determine if phased release period is finished earlier (release to all options is selected after a while), if `trackPhaseRelease` is enabled `phaseReleaseInProgress` will return `NO` only after 7 days of `currentVersionReleaseDate` value set on AppStore Connect. 30 | 31 | - Parameter completion: The completion handler to call when the load request is complete. It returns result that contains UpdatInfo data or PoVError error 32 | 33 | - returns: Discardable `URLSessionDataTask` 34 | */ 35 | @available(swift, obsoleted: 1.0) 36 | @objc(checkForUpdateFromAppStoreWithCompletion:error:) 37 | @discardableResult 38 | public static func checkForUpdateFromAppStore( 39 | completion: @escaping AppStoreObjectCompletionBlock, 40 | error: @escaping ObjectErrorBlock 41 | ) -> URLSessionDataTask? { 42 | return internalyCheckAndPrepareForUpdateAppStore(bundle: .main, trackPhaseRelease: true, callbackQueue: .main, completion: completion, error: error) 43 | } 44 | 45 | /** 46 | Used for getting the versioning information from the AppStore Connect. 47 | 48 | After check with server is finished, this method will return all informations about the app versioning available on AppStore Connect. 49 | It's up to the user to handle that info in a way sutable for the app. 50 | 51 | Update status can only take on value `UpdateStatus.noUpdateAvailable` or `UpdateStatus.newUpdateAvailable`, but it can't be `UpdateStatus.requiredUpdateNeeded` since there is no way to determine if the update version is mandatory with this check. 52 | 53 | If flag `trackPhaseRelease` is set to `NO`, the value of the `phaseReleaseInProgress` will instantly be `NO` as phased release is not used. 54 | Otherwise, if we have to check `trackPhaseRelease`, value of `phaseReleaseInProgress` will return `NO` once phased release period of 7 days is over. 55 | 56 | __WARNING:__ As we are not able to determine if phased release period is finished earlier (release to all options is selected after a while), if `trackPhaseRelease` is enabled `phaseReleaseInProgress` will return `NO` only after 7 days of `currentVersionReleaseDate` value set on AppStore Connect. 57 | 58 | - Parameters: 59 | * trackPhaseRelease: Boolean that indicates whether PoV should notify about new version after 7 days when app is fully rolled out or immediately. Default value is `YES`. 60 | * completion: The completion handler to call when the load request is complete. It returns result that contains UpdatInfo data or PoVError error 61 | 62 | - Returns: 63 | * Discardable `URLSessionDataTask` 64 | */ 65 | @available(swift, obsoleted: 1.0) 66 | @objc(checkForUpdateFromAppStoreWithTrackPhasedRelease:completion:error:) 67 | @discardableResult 68 | public static func checkForUpdateFromAppStore( 69 | trackPhaseRelease: Bool, 70 | completion: @escaping AppStoreObjectCompletionBlock, 71 | error: @escaping ObjectErrorBlock 72 | ) -> URLSessionDataTask? { 73 | return internalyCheckAndPrepareForUpdateAppStore(bundle: .main, trackPhaseRelease: trackPhaseRelease, callbackQueue: .main, completion: completion, error: error) 74 | } 75 | 76 | /** 77 | Used for getting the versioning information from the AppStore Connect. 78 | 79 | After check with server is finished, this method will return all informations about the app versioning available on AppStore Connect. 80 | It's up to the user to handle that info in a way sutable for the app. 81 | 82 | Update status can only take on value `UpdateStatus.noUpdateAvailable` or `UpdateStatus.newUpdateAvailable`, but it can't be `UpdateStatus.requiredUpdateNeeded` since there is no way to determine if the update version is mandatory with this check. 83 | 84 | If flag `trackPhaseRelease` is set to `NO`, the value of the `phaseReleaseInProgress` will instantly be `NO` as phased release is not used. 85 | Otherwise, if we have to check `trackPhaseRelease`, value of `phaseReleaseInProgress` will return `NO` once phased release period of 7 days is over. 86 | 87 | __WARNING:__ As we are not able to determine if phased release period is finished earlier (release to all options is selected after a while), if `trackPhaseRelease` is enabled `phaseReleaseInProgress` will return `NO` only after 7 days of `currentVersionReleaseDate` value set on AppStore Connect. 88 | 89 | If parameter `notificationFrequency` is set to `.always` and latest version of the app is bigger than installed version, method will always return `.newUpdateAvailable`. However, if the`notificationFrequency` is set to `.once`, only first time this method is called for the same latest app version, it will return `.newUpdateAvailable`, each subsequent call, it will return `.noUpdateAvailable`. 90 | 91 | If the optional `country` parameter is provided, the API request will target the specified App Store region (e.g., `"mk"` for Macedonia). If `country` is not provided, the API will fallback to its default behavior, typically fetching data from the U.S. App Store. 92 | 93 | - Parameters: 94 | * bundle: Bundle where .plist file is stored in which app identifier and app versions should be checked. 95 | * trackPhaseRelease: Boolean that indicates whether PoV should notify about new version after 7 days when app is fully rolled out or immediately. Default value is `YES`. 96 | * callbackQueue: The queue on which the completion handler is dispatched. By default, `main` queue is used. 97 | * notificationFrequency: Determines update status appearance frequency. 98 | * country: Optional parameter to specify the App Store region to target (e.g., `"mk"` for Macedonia). Defaults to `nil`. 99 | * completion: The completion handler to call when the load request is complete. It returns result that contains UpdatInfo data or PoVError error 100 | 101 | - Returns: 102 | * Discardable `URLSessionDataTask` 103 | */ 104 | @available(swift, obsoleted: 1.0) 105 | @objc(checkForUpdateFromAppStoreWithTrackPhasedRelease:callbackQueue:bundle:notificationFrequency:country:completion:error:) 106 | @discardableResult 107 | public static func checkForUpdateFromAppStore( 108 | trackPhaseRelease: Bool, 109 | callbackQueue: DispatchQueue, 110 | bundle: Bundle, 111 | notificationFrequency: UpdateNotificationType, 112 | country: String?, 113 | completion: @escaping AppStoreObjectCompletionBlock, 114 | error: @escaping ObjectErrorBlock 115 | ) -> URLSessionDataTask? { 116 | 117 | let frequency: NotificationType = notificationFrequency == .always ? .always : .once 118 | 119 | return internalyCheckAndPrepareForUpdateAppStore( 120 | bundle: bundle, 121 | trackPhaseRelease: trackPhaseRelease, 122 | callbackQueue: callbackQueue, 123 | notificationFrequency: frequency, 124 | country: country, 125 | completion: completion, 126 | error: error 127 | ) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /JSON.md: -------------------------------------------------------------------------------- 1 | # JSON File 2 | 3 | ## JSON-format 4 | 5 | JSON file in your application has to follow [Semantic Versioning](http://semver.org/) and it has to look like this: 6 | 7 | ```json 8 | { 9 | "ios":[ 10 | { 11 | "required_version":"1.2.3", 12 | "last_version_available":"1.9.0", 13 | "notify_last_version_frequency":"ALWAYS", 14 | "requirements":{ 15 | "required_os_version":"8.0.0", 16 | "region":"hr", 17 | "bluetooth":"5.0" 18 | }, 19 | "meta":{ 20 | "key1":"value1", 21 | "key2":2 22 | } 23 | } 24 | ], 25 | "macos":[ 26 | { 27 | "required_version":"10.10.0", 28 | "last_version_available":"11.0", 29 | "notify_last_version_frequency":"ALWAYS", 30 | "requirements":{ 31 | "required_os_version":"10.12.1", 32 | "region":"hr", 33 | "bluetooth":"5.0" 34 | } 35 | } 36 | ], 37 | "meta":{ 38 | "key3":true, 39 | "key4":"value2" 40 | } 41 | } 42 | ``` 43 | 44 | Library will decide which configuration to use based on the platform used and requirements listed under `requirements` key. 45 | 46 | > First configuration that meets all the requirements will be used to determine update status. 47 | 48 | For more details about requirements, check out [this section](#Requirements). 49 | 50 | * **Notification frequency** 51 | 52 | Depending on `notify_last_version_frequency` property, the user can be notified `ONCE` or `ALWAYS`. The library handles this for you. If notification frequency is set to `ONCE`, in the result values which are returned, the value of `updateStatus` will be set to `UpdateStatus.newUpdateAvailable`. Every subsequent call, the library will set the value of `updateStatus` to `UpdateStatus.noUpdateAvailable` for that specific version. 53 | 54 | * **Metadata** 55 | 56 | Key-value pairs under `"meta"` key are optional metadata of which any amount can be sent accompanying the required fields. Metadata can be specified for each configuration and it can also be specified on a global level. In the return values, global metadata and metadata from the appropriate configuration will be merged. If there is not an appropriate configuration, only global metadata will be returned. 57 | 58 | ## Supporting older versions (< 4.0) 59 | 60 | To support PrinceOfVersions versions less than 4.0, JSON file will look somewhat different. 61 | 62 | JSON still has to follow [Semantic Versioning](http://semver.org/). 63 | 64 | * **PrinceOfVersions version < 4.0** 65 | 66 | You have to provide an configuration object which conforms older PrinceOfVersions (< 4.0) specification under key `ios` or `macos`, depending on a platform. 67 | 68 | * **PrinceOfVersions version >= 4.0** 69 | 70 | You have to specify configuration described in [previous section](#JSON-format). Configuration should be stored under key `ios2` or `macos2`. 71 | 72 | Described JSON format is displayed below: 73 | 74 | ```json 75 | { 76 | "ios":{ 77 | "minimum_version":"1.2.3", 78 | "minimum_version_min_sdk":"8.0.0", 79 | "latest_version":{ 80 | "version":"2.4.5", 81 | "notification_type":"ALWAYS", 82 | "min_sdk":"12.1.2" 83 | } 84 | }, 85 | "ios2":[ 86 | { 87 | "required_version":"1.2.3", 88 | "last_version_available":"1.9.0", 89 | "notify_last_version_frequency":"ALWAYS", 90 | "requirements":{ 91 | "required_os_version":"8.0.0", 92 | "region":"hr", 93 | "bluetooth":"5.0" 94 | }, 95 | "meta":{ 96 | "key1":"value1", 97 | "key2":2 98 | } 99 | } 100 | ], 101 | "macos":{ 102 | "minimum_version":"1.2.3", 103 | "minimum_version_min_sdk":"10.9.0", 104 | "latest_version":{ 105 | "version":"2.4.5", 106 | "notification_type":"ALWAYS", 107 | "min_sdk":"10.11.0" 108 | } 109 | }, 110 | "macos2":[ 111 | { 112 | "required_version":"10.10.0", 113 | "last_version_available":"11.0", 114 | "notify_last_version_frequency":"ALWAYS", 115 | "requirements":{ 116 | "required_os_version":"10.12.1" 117 | } 118 | } 119 | ], 120 | "meta":{ 121 | "key3":true, 122 | "key4":"value2" 123 | } 124 | } 125 | ``` 126 | 127 | ## Requirements 128 | 129 | For every configuration, there is a possibility to define requirements. Based on the provided requirements and user-provided requirements checks, the appropriate configuration will be chosen and evaluated for update status. 130 | 131 | > If you don't provide any requirements for a configuration in JSON, that configuration will be classified as valid. 132 | 133 | Defining requirements is possible through `requirements array`. It is up to you to choose which requirements are necessary for your configuration. However, for setting required operating system version requirement, you can use key `required_os_version`. By using that key, library will provide requirement check so the user doesn't have to define it. Every other requirement in configuration will be checked with closures provided by the user. Closure can be provided by `addRequirement` [method](README.md#Adding-requirements) in `PoVRequestOptions` class. If requirement closure is not supplied for a given requirement key, library will consider that requirement as **not satisfied**. 134 | 135 | > If there is not even one configuration that satisfies all requirements (including `required_os_version`), library will return error with value `requirementsNotSatisfied`. 136 | 137 | So to sum up, chosen configuration depends on requirements in JSON, requirement checks provided by the user and it's position in configurations array in JSON. For more info, please check [example section](#Examples). 138 | 139 | ### Examples 140 | 141 | * Configurations without requirements 142 | 143 | ```json 144 | ... 145 | "ios":[ 146 | { 147 | "required_version":"1.2.4", 148 | "last_version_available":"1.8.0", 149 | "notify_last_version_frequency":"ALWAYS" 150 | }, 151 | { 152 | "required_version":"1.2.3", 153 | "last_version_available":"1.8.0", 154 | "notify_last_version_frequency":"ONCE" 155 | } 156 | ] 157 | ... 158 | ``` 159 | 160 | In this example, first configuration will always be chosen whether or not user provides requirement checks. 161 | 162 | * Configurations with increasing requirements count 163 | 164 | ```json 165 | { 166 | "macos":[ 167 | { 168 | "required_version":"10.10.0", 169 | "last_version_available":"11.0", 170 | "notify_last_version_frequency":"ALWAYS", 171 | "requirements":{ 172 | "required_os_version":"10.12.1" 173 | } 174 | }, 175 | { 176 | "required_version":"9.1", 177 | "last_version_available":"11.0", 178 | "notify_last_version_frequency":"ALWAYS", 179 | "requirements":{ 180 | "required_os_version":"10.11.1", 181 | "region":"hr", 182 | } 183 | }, 184 | { 185 | "required_version":"9.0", 186 | "last_version_available":"11.0", 187 | "notify_last_version_frequency":"ONCE", 188 | "requirements":{ 189 | "required_os_version":"10.14.2", 190 | "region":"us", 191 | "bluetooth":"5.0" 192 | } 193 | }, 194 | ] 195 | } 196 | ``` 197 | 198 | For the sake of this example, let's say that all required OS versions are met. In this example, the first configuration will always be chosen since all requirements are met, second and third configuration won't be evaluated no matter if user defined requirement checks for `region` or `Bluetooth`. 199 | 200 | * Configuration with decreasing requirements count 201 | 202 | ```json 203 | { 204 | "macos":[ 205 | { 206 | "required_version":"9.0", 207 | "last_version_available":"11.0", 208 | "notify_last_version_frequency":"ONCE", 209 | "requirements":{ 210 | "required_os_version":"10.14.2", 211 | "region":"us", 212 | "bluetooth":"5.0" 213 | } 214 | }, 215 | { 216 | "required_version":"9.1", 217 | "last_version_available":"11.0", 218 | "notify_last_version_frequency":"ALWAYS", 219 | "requirements":{ 220 | "required_os_version":"10.11.1", 221 | "region":"hr" 222 | } 223 | }, 224 | { 225 | "required_version":"10.10.0", 226 | "last_version_available":"11.0", 227 | "notify_last_version_frequency":"ALWAYS", 228 | "requirements":{ 229 | "required_os_version":"10.12.1" 230 | } 231 | } 232 | ] 233 | } 234 | ``` 235 | 236 | Here we will also consider `required_os_version` requirement as met. If user provided requirement checks for both region and Bluetooth and they are both met, first configuration will be chosen. If user provided check only for `region`, only second and third configuration would be considered, since `bluetooth` requirement is not met. 237 | 238 | ## Final thoughts 239 | 240 | * You can define requirements in any kind of way that you want and in any sort of order, but be aware that their order profoundly affects the way they are chosen. 241 | * Best practice would be to put configuration without any requirements (or only with `required_os_version` requirement) on the bottom of the list since all other configurations after this one will be ignored. 242 | 243 | 244 | [:arrow_left: Go back](README.md) 245 | -------------------------------------------------------------------------------- /Tests/PrinceOfVersionsTests/mockdata/app_store_version_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "resultCount": 1, 3 | "results": 4 | [ 5 | { 6 | "ipadScreenshotUrls": 7 | [ 8 | "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/c8/66/16/c86616ae-2fc9-ccdf-109c-f3eae01371ef/fbbd14e5-01a3-4f6c-9b5a-a580129602c6_01_Store_tablet.png/552x414bb.png", 9 | "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/77/d7/4b/77d74bd9-c00c-b59c-fa2a-e5e3685274f8/775eedaa-0251-4936-aba6-3263a944919e_02_Store_tablet.png/552x414bb.png", 10 | "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/ec/4e/e3/ec4ee3a2-3bb5-6512-710f-d1bb03076423/78088876-335b-469f-b43e-bdf61d9f03a2_03_Store_tablet.png/552x414bb.png", 11 | "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/91/0d/98/910d98c9-15c1-c88e-5585-9d2e25ee83b2/195cb6e1-857c-4139-825f-9dff981306e7_04_Store_tablet.png/552x414bb.png", 12 | "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource116/v4/3a/88/82/3a8882e7-434d-c69f-6478-7b696aa562f6/2c931f79-3fb2-4231-808e-e79b82546c54_05_Store_tablet.png/552x414bb.png", 13 | "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/e5/c2/b7/e5c2b7e6-f5b1-f486-9b86-ef09680c1eaa/fd9483b0-bade-4fa0-bb4e-f3aa15dc1bc6_06_Store_tablet.png/552x414bb.png" 14 | ], 15 | "appletvScreenshotUrls": 16 | [], 17 | "isGameCenterEnabled": false, 18 | "supportedDevices": 19 | [ 20 | "iPhone5s-iPhone5s", 21 | "iPadAir-iPadAir", 22 | "iPadAirCellular-iPadAirCellular", 23 | "iPadMiniRetina-iPadMiniRetina", 24 | "iPadMiniRetinaCellular-iPadMiniRetinaCellular", 25 | "iPhone6-iPhone6", 26 | "iPhone6Plus-iPhone6Plus", 27 | "iPadAir2-iPadAir2", 28 | "iPadAir2Cellular-iPadAir2Cellular", 29 | "iPadMini3-iPadMini3", 30 | "iPadMini3Cellular-iPadMini3Cellular", 31 | "iPodTouchSixthGen-iPodTouchSixthGen", 32 | "iPhone6s-iPhone6s", 33 | "iPhone6sPlus-iPhone6sPlus", 34 | "iPadMini4-iPadMini4", 35 | "iPadMini4Cellular-iPadMini4Cellular", 36 | "iPadPro-iPadPro", 37 | "iPadProCellular-iPadProCellular", 38 | "iPadPro97-iPadPro97", 39 | "iPadPro97Cellular-iPadPro97Cellular", 40 | "iPhoneSE-iPhoneSE", 41 | "iPhone7-iPhone7", 42 | "iPhone7Plus-iPhone7Plus", 43 | "iPad611-iPad611", 44 | "iPad612-iPad612", 45 | "iPad71-iPad71", 46 | "iPad72-iPad72", 47 | "iPad73-iPad73", 48 | "iPad74-iPad74", 49 | "iPhone8-iPhone8", 50 | "iPhone8Plus-iPhone8Plus", 51 | "iPhoneX-iPhoneX", 52 | "iPad75-iPad75", 53 | "iPad76-iPad76", 54 | "iPhoneXS-iPhoneXS", 55 | "iPhoneXSMax-iPhoneXSMax", 56 | "iPhoneXR-iPhoneXR", 57 | "iPad812-iPad812", 58 | "iPad834-iPad834", 59 | "iPad856-iPad856", 60 | "iPad878-iPad878", 61 | "iPadMini5-iPadMini5", 62 | "iPadMini5Cellular-iPadMini5Cellular", 63 | "iPadAir3-iPadAir3", 64 | "iPadAir3Cellular-iPadAir3Cellular", 65 | "iPodTouchSeventhGen-iPodTouchSeventhGen", 66 | "iPhone11-iPhone11", 67 | "iPhone11Pro-iPhone11Pro", 68 | "iPadSeventhGen-iPadSeventhGen", 69 | "iPadSeventhGenCellular-iPadSeventhGenCellular", 70 | "iPhone11ProMax-iPhone11ProMax", 71 | "iPhoneSESecondGen-iPhoneSESecondGen", 72 | "iPadProSecondGen-iPadProSecondGen", 73 | "iPadProSecondGenCellular-iPadProSecondGenCellular", 74 | "iPadProFourthGen-iPadProFourthGen", 75 | "iPadProFourthGenCellular-iPadProFourthGenCellular", 76 | "iPhone12Mini-iPhone12Mini", 77 | "iPhone12-iPhone12", 78 | "iPhone12Pro-iPhone12Pro", 79 | "iPhone12ProMax-iPhone12ProMax", 80 | "iPadAir4-iPadAir4", 81 | "iPadAir4Cellular-iPadAir4Cellular", 82 | "iPadEighthGen-iPadEighthGen", 83 | "iPadEighthGenCellular-iPadEighthGenCellular", 84 | "iPadProThirdGen-iPadProThirdGen", 85 | "iPadProThirdGenCellular-iPadProThirdGenCellular", 86 | "iPadProFifthGen-iPadProFifthGen", 87 | "iPadProFifthGenCellular-iPadProFifthGenCellular", 88 | "iPhone13Pro-iPhone13Pro", 89 | "iPhone13ProMax-iPhone13ProMax", 90 | "iPhone13Mini-iPhone13Mini", 91 | "iPhone13-iPhone13", 92 | "iPadMiniSixthGen-iPadMiniSixthGen", 93 | "iPadMiniSixthGenCellular-iPadMiniSixthGenCellular", 94 | "iPadNinthGen-iPadNinthGen", 95 | "iPadNinthGenCellular-iPadNinthGenCellular", 96 | "iPhoneSEThirdGen-iPhoneSEThirdGen", 97 | "iPadAirFifthGen-iPadAirFifthGen", 98 | "iPadAirFifthGenCellular-iPadAirFifthGenCellular", 99 | "iPhone14-iPhone14", 100 | "iPhone14Plus-iPhone14Plus", 101 | "iPhone14Pro-iPhone14Pro", 102 | "iPhone14ProMax-iPhone14ProMax", 103 | "iPadTenthGen-iPadTenthGen", 104 | "iPadTenthGenCellular-iPadTenthGenCellular", 105 | "iPadPro11FourthGen-iPadPro11FourthGen", 106 | "iPadPro11FourthGenCellular-iPadPro11FourthGenCellular", 107 | "iPadProSixthGen-iPadProSixthGen", 108 | "iPadProSixthGenCellular-iPadProSixthGenCellular" 109 | ], 110 | "features": 111 | [ 112 | "iosUniversal" 113 | ], 114 | "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple126/v4/f5/fa/d3/f5fad360-5917-341e-4746-85dede33c0f7/AppIcon-0-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/60x60bb.jpg", 115 | "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple126/v4/f5/fa/d3/f5fad360-5917-341e-4746-85dede33c0f7/AppIcon-0-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/512x512bb.jpg", 116 | "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple126/v4/f5/fa/d3/f5fad360-5917-341e-4746-85dede33c0f7/AppIcon-0-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/100x100bb.jpg", 117 | "artistViewUrl": "https://apps.apple.com/us/developer/infinum/id411692409?uo=4", 118 | "screenshotUrls": 119 | [ 120 | "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/7a/a0/aa/7aa0aaf6-ca2f-af95-8bfd-202f70560cc8/2ded49ea-da9c-4216-ab70-744f6f622b42_01_Store_screens.png/392x696bb.png", 121 | "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource116/v4/3f/01/28/3f01286f-bad7-61d8-c6b2-838e02ff2ac9/5b925e44-96c5-416f-a74e-855534df1987_02_Store_screens.png/392x696bb.png", 122 | "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/55/46/14/55461409-a8da-57b4-39fd-ba345cdcbac5/ea3e4d7e-1116-4416-8b97-43b43bbdbd96_03_Store_screens.png/392x696bb.png", 123 | "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource116/v4/0f/ed/23/0fed230b-e02d-93d7-fb14-e1e064d27bc7/6ba4541f-0513-4a08-850f-36268ec93a9e_04_Store_screens.png/392x696bb.png", 124 | "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/b2/27/59/b2275976-bbcc-35e7-7f5a-fa60af55f881/b88a77ef-9ede-45d9-8670-2d062bb7371a_05_Store_screens.png/392x696bb.png", 125 | "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/9b/53/d3/9b53d307-b1bc-9310-59bf-fad33cc10a38/243d8ba6-cd67-440a-b560-f24076d6b469_06_Store_screens.png/392x696bb.png", 126 | "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource116/v4/e0/a8/09/e0a809b5-dd95-0caa-77d9-fe434651f8cd/8024c371-c633-49a3-b61a-4602d9b6562d_07_Store_screens.png/392x696bb.png", 127 | "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource126/v4/15/1d/1d/151d1d78-e845-4fc7-9e8b-660cfd805ffd/920b1f58-715c-4beb-8e93-67c9af0d446a_08_Store_screens.png/392x696bb.png" 128 | ], 129 | "advisories": 130 | [], 131 | "kind": "software", 132 | "currentVersionReleaseDate": "2023-06-27T20:23:21Z", 133 | "releaseNotes": "- Subsidiary field will now show when necessary when creating a new budget or deal\n- Grouped custom fields when editing multiple records in tables\n- Added the currency switcher to the dashboard widget\n- Fixed some issues with overhead\n- Fixed an issue where budgets on a project weren't visible\n- Fixed a bug with the entitlement form\n- Fixed some bugs related to sorting and creating tasks\n- Various other fixes and improvements", 134 | "artistId": 411692409, 135 | "artistName": "Infinum", 136 | "genres": 137 | [ 138 | "Business", 139 | "Productivity" 140 | ], 141 | "price": 0.00, 142 | "description": "Productive is the most powerful & yet simple way to manage a profitable agency or consulting business. Get work done, track time, monitor financials, manage contacts, collaborate with clients, and more.\n\nThe Productive mobile app gives you quick and easy access to your agency business while you're out and about.\n\nUsing the app, you can:\n - Track time\n - Create, manage, and comment on tasks for all your projects\n - View and upload attachments\n - Track profitability for budgets\n - Get notifications when something happens on a certain task or deal\n - Get an overview of your sales pipeline\n - Manage your contacts\n - Switch between different organizations you're involved with\n - ...and much, much more\n\nProductive is the only tool you need to run a profitable agency.", 143 | "bundleId": "com.infinum.productive", 144 | "trackId": 958607533, 145 | "trackName": "Productive - Run your agency", 146 | "releaseDate": "2015-09-23T19:35:36Z", 147 | "primaryGenreName": "Business", 148 | "primaryGenreId": 6000, 149 | "isVppDeviceBasedLicensingEnabled": true, 150 | "sellerName": "Infinum d.o.o.", 151 | "genreIds": 152 | [ 153 | "6000", 154 | "6007" 155 | ], 156 | "currency": "USD", 157 | "trackViewUrl": "https://apps.apple.com/us/app/productive-run-your-agency/id958607533?uo=4", 158 | "minimumOsVersion": "12.0", 159 | "trackCensoredName": "Productive - Run your agency", 160 | "languageCodesISO2A": 161 | [ 162 | "EN" 163 | ], 164 | "fileSizeBytes": "112804864", 165 | "sellerUrl": "https://productive.io/", 166 | "formattedPrice": "Free", 167 | "contentAdvisoryRating": "4+", 168 | "averageUserRatingForCurrentVersion": 5, 169 | "userRatingCountForCurrentVersion": 3, 170 | "averageUserRating": 5, 171 | "trackContentRating": "4+", 172 | "version": "0.1.0", 173 | "wrapperType": "software", 174 | "userRatingCount": 3 175 | } 176 | ] 177 | } 178 | --------------------------------------------------------------------------------