├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------