From 53b965effce9a04e9ae18d0a528b017fae0d5254 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Fri, 7 Feb 2020 10:24:48 +0100 Subject: [PATCH] [feature/licensing] Licensing support (#571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - Initial OCLicense draft * - Start expanding concept with OCLicenseEnvironment and provide first pseudo example code * OCLicensing: - advance first implementation - add first unit tests * Autoplay Media / Show album artwork in the media player view (#566) * [fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547) * - Fix Swift and SwiftLint warnings - Remove unused UploadsSettingsSection (was replaced by MediaUploadSettings) * - address libzip Xcode project upgrade warning - add research note to FileProviderExtension - update SDK * - Allow offline browsing of folders in the File Provider * Revert "[fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547)" (#553) This reverts commit 9a0bc93215b786e715b0a89cc9aa793fd121407b. * Autoplay media files implemented as described in issue #59 * Added album artwork as overlay in the player * Fixed playing next media item in BG and lock screen - Now multiple items can be played contignuously in the background - Now playing info in the lock screen contains artwork, title, artist info and displays correct playback timeline - Audio can be paused / resumed from the lock screen - Added skip controls allowing to jump 10s backwards and forwards from the play-head position in the lock screen * Small fixes * [fix/open-in-on-ipad] Share sheet not visible on iPad (#570) * [fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547) * - Fix Swift and SwiftLint warnings - Remove unused UploadsSettingsSection (was replaced by MediaUploadSettings) * - address libzip Xcode project upgrade warning - add research note to FileProviderExtension - update SDK * - Allow offline browsing of folders in the File Provider * Revert "[fix/fp-offline-browsing] Allow offline browsing of folders in the File Provider (#547)" (#553) This reverts commit 9a0bc93215b786e715b0a89cc9aa793fd121407b. * Fix for issue #568: Share sheet was now visible on iPad, if tableview was scrolled down, after first visible page rect * - implemented OCLicenseManager - implemented OCLicenseProvider - implemented App Store Receipt parser, helper classes, helper categories and receipt object tree - extended OCLicenseEnvironment with bookmark UUID - removed OCLicenseTrialProvider (=> can be handled through App Store IAPs via updated guidelines) - added OCLicenseDuration - added OCLicenseTransaction - added InAppPurchasesReceiptViewController (to be removed again) - added LicenseTransactionsViewController (which sparked OCLicenseTransaction) - added LicenseOffersViewController (testing only) - list of changes is incomplete, but commit needed to bring in changes from the origin branch * - Licensing additions: - extend OCLicenseEnvironment with more possible properties - add OCCore+LicenseEnvironment to be able to get a OCLicenseEnvironment directly for every OCCore - add OCLicenseEnterpriseProvider to selectively unlock products if connected to an Enterprise instance - Add License gating support to Action - LicenseRequirements to encapsulate license requirements - isLicensed: quick way to determine if an Action is licensed - proceedWithLicensing: method to check licensing status and present licensing options if not licensed; allows simple gating - add "PRO" labeling to actions that are unlicensed but require licensing - ScanAction uses new License gating - adapt LicenseOffersViewController to be usable from Action license gating as a POC implementation (that can serve as starting point for the full implementation) - LicenseTransactionsViewController now refreshes the view after restoring purchases - removed InAppPurchasesReceiptViewController as it was replaced by LicenseTransactionsViewController - fix various warnings in other parts of milestone/1.2 * - Improve layout of license offers view controller * - remove IAP-based "trial.pro.30days" trial AppStoreItem - make MoreViewController properly add provided view controllers as child view controllers (fixing broken presentation from these) - LicenseOffers: new view, button and view controller classes for flexible presentation of IAPs and subscriptions - OCLicenseManager+AppStore: utility IAP functions like f.ex. to restore purchases - LicenseTransactionsViewController: add support for adding (a) link(s) at the bottom of a transaction - ThemeButton: provide additional options to configure the inner padding and corner rounding of the button - ownCloudApp/OCLicensing: - OCLicenseDuration: .localizedDescription now returns just the word for single units ("month" instead of "1 month") - OCLicenseOffer: - new OCLicenseOfferStateExpired state to differentiate between a subscription that has already been subscribed to in the past but expired, and those that the user has not yet ever subscribed to - stateInEnvironment now also performs a feature-level check to ensure offers for whose features valid entitlements already exist are OCLicenseOfferStateRedundant - OCLicenseTransaction: - added .links property to allow adding links to a transaction - OCLicenseAppStoreProvider: - .purchasesAllowed property provides info on whether purchases are allowed on this device - active subscription transactions now also include a link to manage the subscription - the state of offers is updated as transactions happen - OCLicenseAppStoreReceipt: - fix bug that would drop all but the last IAP in a receipt * - Fix warnings from merge with milestone/1.3 * ios-app: - move license management setup to ownCloudAppShared framework - move all app-side licensing code to ownCloudAppShared framework ownCloudApp: - add new OCLicenseEnvironment(withBookmark:) convenience method - fix unit test errors ownCloudAppShared: - create new IntentSettings singleton to manage enabled and purchased states across all intents - adopt IntentSettings in all existing Intents * - fix code signing settings for Intents appex * ios-app: - ThemeCollection: add .purchaseColors set for purchase buttons - ThemeItemStyle, NSObject+ThemeApplication: add .purchase item style - StaticTableRow: add actionTriggered() action method - Settings - PurchasesSettingsSection: add dedicated settings section for IAPs - LicenseTransactionsViewController: add sorting to ensure that the latest purchases are shown first - LicenseInAppProductListViewController, LicenseInAppPurchaseFeatureView: provides an overview of Pro Features, their status and purchase/subscription options - SDK update to be able to use new OCBookmark.userInfo.statusInfo data ownCloudApp: - OCLicenseEnvironment - extend with .bookmark property - make .bookmarkUUID and .bookmark dynamic - OCLicenseFeature: extend with optional .localizedName and .localizedDescription properties - OCLicenseManager: add method to retrieve all features, or only features for which offers exist - OCLicenseAppStoreProvider: - fix import typo - reload receipt after SKPaymentTransactions updates have been processed - take subscription expiration date into account when updating the state of OCLicenseOffers - add setReceiptNeedsReload - OCLicenseEnterpriseProvider: extend applicability rule to also use Enterprise edition hint in bookmark.userInfo - OCLicenseTransaction: put the product name in the first table row ownCloudAppShared: - complete IAP setup and descriptions - change Intents .unlicensed error message to (hopefully) be suitable in all situations * ownCloudAppFramework: - OCLicenseAppStoreProvider: - break out product request handling into its own method - add convenience method that allows loading of products if previous requests failed - Fix error in and extend Licensing README.md ownCloudAppShared: - IntentSettings: add class settings support App: - add missing localizations - add error handling to when no product list can be loaded from the App Store - turn "PRO" label into a variable of its own - remove test code * - change Intents error message from "Premium Feature" to "Pro Feature" (addressing (1)). - hide IAPs in versions of iOS preceeding version 13 (addressing (5)). * - Remove trailing semicolon * App: - LicenseOfferView: add error handler that present an alert AppFramework/Licensing: - turn OCLicenseOfferCommitOption into a typed enum - add OCLicenseOfferCommitErrorHandler type to handle errors from the user committing to an offer - extend OCLicenseOffer.commit() with support for an error handler - OCLicenseAppStoreProvider - add error domain and missing code for when purchases are not allowed - add error handler tracking and calls in the appropriate places - add new Localized.strings file to localize "Purchases are not allowed on this device." errors * Address finding (8) in PR #571 - LicenseOfferButton: preserves original title for recovery - LicenseOfferView: change back from "Unlocked" button title if appropriate - OCLicenseAppStoreProvider: recompute offers after (re-)loading the receipt * ownCloudApp.framework/Licensing: - OCLicenseEntitlement: add debug description - OCLicenseManager: - add log tagging - add additional logging - add new API that allows to perform blocks only after all pending internal refreshes have been carried out - OCLicenseAppStoreProvider: - add IPC notification support to notify app extensions of changes to the receipt ownCloudAppShared.framework: - change IntentSettings.isLicensedFor() to support asynchronous computation as well as optimized waiting for pending refreshes, fixing finding (9) in feature/licensing (#571) * - Update SDK Co-authored-by: Michael Neuwert Co-authored-by: Matthias Hühne --- ios-sdk | 2 +- ownCloud.xcodeproj/project.pbxproj | 436 ++++++++- .../xcshareddata/xcschemes/ownCloud.xcscheme | 7 +- .../xcschemes/ownCloudApp.xcscheme | 94 ++ ownCloud/AppDelegate.swift | 8 +- ownCloud/Client/Actions/Action.swift | 59 +- .../Client/Actions/MoreViewController.swift | 3 + .../Client/Actions/Scanner/ScanAction.swift | 7 + .../Viewer/DisplayHostViewController.swift | 10 +- .../Media/MediaDisplayViewController.swift | 19 +- ownCloud/Key Commands/KeyCommands.swift | 2 +- ownCloud/Licensing/LicenseRequirements.swift | 28 + .../Licensing/Offers/LicenseOfferButton.swift | 62 ++ .../Licensing/Offers/LicenseOfferView.swift | 282 ++++++ .../Offers/LicenseOffersViewController.swift | 148 +++ ...icenseInAppProductListViewController.swift | 94 ++ .../LicenseInAppPurchaseFeatureView.swift | 135 +++ .../Tools/OCLicenseManager+AppStore.swift | 55 ++ .../LicenseTransactionsViewController.swift | 112 +++ .../MediaUploadActivity.swift | 2 +- .../MediaUploadQueue.swift | 5 +- .../Resources/en.lproj/Localizable.strings | 47 + .../SDK Extensions/OCBookmark+Extension.swift | 2 +- ownCloud/Settings/MoreSettingsSection.swift | 3 +- .../Settings/PurchasesSettingsSection.swift | 63 ++ .../Settings/SettingsViewController.swift | 5 + .../InstantMediaUploadTaskExtension.swift | 6 +- .../Theming/NSObject+ThemeApplication.swift | 5 + ownCloud/Theming/ThemeCollection.swift | 5 + ownCloud/Theming/UI/ThemeButton.swift | 35 +- .../CardPresentationController.swift | 2 +- ownCloud/UI Elements/StaticTableViewRow.swift | 4 + .../UIAlertController+OCIssue.swift | 4 +- ownCloud/Window/OpenItemUserActivity.swift | 2 +- .../OCCore+LicenseEnvironment.h | 30 + .../OCCore+LicenseEnvironment.m | 34 + .../Entitlement/OCLicenseEntitlement.h | 53 ++ .../Entitlement/OCLicenseEntitlement.m | 109 +++ .../Environment/OCLicenseEnvironment.h | 47 + .../Environment/OCLicenseEnvironment.m | 84 ++ .../Licensing/Feature/OCLicenseFeature.h | 47 + .../Licensing/Feature/OCLicenseFeature.m | 72 ++ .../Manager/OCLicenseManager+Internal.h | 20 + .../Licensing/Manager/OCLicenseManager.h | 74 ++ .../Licensing/Manager/OCLicenseManager.m | 861 ++++++++++++++++++ .../Licensing/Manager/OCLicenseObserver.h | 40 + .../Licensing/Manager/OCLicenseObserver.m | 85 ++ .../Licensing/OCLicenseTypes.h | 59 ++ .../Licensing/Offer/OCLicenseDuration.h | 56 ++ .../Licensing/Offer/OCLicenseDuration.m | 166 ++++ .../Licensing/Offer/OCLicenseOffer.h | 91 ++ .../Licensing/Offer/OCLicenseOffer.m | 196 ++++ .../Licensing/Product/OCLicenseProduct.h | 52 ++ .../Licensing/Product/OCLicenseProduct.m | 82 ++ .../App Store/Items/OCLicenseAppStoreItem.h | 49 + .../App Store/Items/OCLicenseAppStoreItem.m | 52 ++ .../App Store/OCLicenseAppStoreProvider.h | 70 ++ .../App Store/OCLicenseAppStoreProvider.m | 813 +++++++++++++++++ .../AppleIncRootCertificate.cer | Bin 0 -> 1215 bytes .../App Store/Parser Support/NSDate+RFC3339.h | 29 + .../App Store/Parser Support/NSDate+RFC3339.m | 65 ++ .../App Store/Parser Support/OCASN1.h | 41 + .../App Store/Parser Support/OCASN1.m | 219 +++++ .../Receipt/OCLicenseAppStoreReceipt.h | 107 +++ .../Receipt/OCLicenseAppStoreReceipt.m | 240 +++++ .../OCLicenseAppStoreReceiptInAppPurchase.h | 48 + .../OCLicenseAppStoreReceiptInAppPurchase.m | 80 ++ .../Enterprise/OCLicenseEnterpriseProvider.h | 33 + .../Enterprise/OCLicenseEnterpriseProvider.m | 57 ++ .../Licensing/Providers/OCLicenseProvider.h | 59 ++ .../Licensing/Providers/OCLicenseProvider.m | 114 +++ ownCloudAppFramework/Licensing/README.md | 122 +++ .../Transactions/OCLicenseTransaction.h | 58 ++ .../Transactions/OCLicenseTransaction.m | 168 ++++ .../Resources/en.lproj/Localized.strings | 9 + ownCloudAppFramework/ownCloudApp.h | 23 + ownCloudAppFrameworkTests/LicensingTests.m | 486 ++++++++++ ownCloudAppFrameworkTests/ownCloudAppTests.m | 37 - .../Base.lproj/Intents.intentdefinition | 36 +- .../Intent/CreateFolderIntentHandler.swift | 15 +- .../Intent/DeletePathItemIntentHandler.swift | 15 +- .../OCBookmarkManager+Extension.swift | 4 +- .../Extensions/OCLicenseManager+Setup.swift | 72 ++ .../Intent/GetAccountIntentHandler.swift | 19 +- .../Intent/GetAccountsIntentHandler.swift | 7 +- .../GetDirectoryListingIntentHandler.swift | 16 +- .../Intent/GetFileInfoIntentHandler.swift | 15 +- .../Intent/GetFileIntentHandler.swift | 15 +- ownCloudAppShared/Intent/IntentSettings.swift | 90 ++ .../Intent/PathExistsIntentHandler.swift | 15 +- .../Intent/SaveFileIntentHandler.swift | 15 +- ownCloudAppShared/Tools/AppLockHelper.swift | 12 +- 92 files changed, 7174 insertions(+), 162 deletions(-) create mode 100644 ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloudApp.xcscheme create mode 100644 ownCloud/Licensing/LicenseRequirements.swift create mode 100644 ownCloud/Licensing/Offers/LicenseOfferButton.swift create mode 100644 ownCloud/Licensing/Offers/LicenseOfferView.swift create mode 100644 ownCloud/Licensing/Offers/LicenseOffersViewController.swift create mode 100644 ownCloud/Licensing/Product List/LicenseInAppProductListViewController.swift create mode 100644 ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift create mode 100644 ownCloud/Licensing/Tools/OCLicenseManager+AppStore.swift create mode 100644 ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift create mode 100644 ownCloud/Settings/PurchasesSettingsSection.swift create mode 100644 ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.h create mode 100644 ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m create mode 100644 ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.h create mode 100644 ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.m create mode 100644 ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.h create mode 100644 ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m create mode 100644 ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.h create mode 100644 ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.m create mode 100644 ownCloudAppFramework/Licensing/Manager/OCLicenseManager+Internal.h create mode 100644 ownCloudAppFramework/Licensing/Manager/OCLicenseManager.h create mode 100644 ownCloudAppFramework/Licensing/Manager/OCLicenseManager.m create mode 100644 ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.h create mode 100644 ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.m create mode 100644 ownCloudAppFramework/Licensing/OCLicenseTypes.h create mode 100644 ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.h create mode 100644 ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.m create mode 100644 ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.h create mode 100644 ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.m create mode 100644 ownCloudAppFramework/Licensing/Product/OCLicenseProduct.h create mode 100644 ownCloudAppFramework/Licensing/Product/OCLicenseProduct.m create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.h create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.m create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/AppleIncRootCertificate.cer create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.h create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.m create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.h create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.m create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.h create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.m create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.h create mode 100644 ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.m create mode 100644 ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.h create mode 100644 ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.m create mode 100644 ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.h create mode 100644 ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.m create mode 100644 ownCloudAppFramework/Licensing/README.md create mode 100644 ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.h create mode 100644 ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.m create mode 100644 ownCloudAppFramework/Resources/en.lproj/Localized.strings create mode 100644 ownCloudAppFrameworkTests/LicensingTests.m delete mode 100644 ownCloudAppFrameworkTests/ownCloudAppTests.m create mode 100644 ownCloudAppShared/Intent/Extensions/OCLicenseManager+Setup.swift create mode 100644 ownCloudAppShared/Intent/IntentSettings.swift diff --git a/ios-sdk b/ios-sdk index 4047a738e..6276ab723 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 4047a738ef01f43ca7a43b747fd8e29bb4c99e1e +Subproject commit 6276ab7233d435c226e19f2a121989ec8f533411 diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 2afce5c1d..4b83c6f7d 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -198,6 +198,11 @@ DC018F8C20A1060A00135198 /* ProgressHUDViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC018F8B20A1060A00135198 /* ProgressHUDViewController.swift */; }; DC0196AB20F7690C00C41B78 /* OCBookmark+FileProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DC27A1A420CBEF85008ACB6C /* OCBookmark+FileProvider.m */; }; DC01CDCC212EDDF600FC8E38 /* TextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC01CDCB212EDDF600FC8E38 /* TextViewController.swift */; }; + DC080CE5238AE3F40044C5D2 /* OCLicenseAppStoreProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DC080CE3238AE3ED0044C5D2 /* OCLicenseAppStoreProvider.m */; }; + DC080CE6238AE3F40044C5D2 /* OCLicenseAppStoreProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DC080CE2238AE3ED0044C5D2 /* OCLicenseAppStoreProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC080CF1238C8D850044C5D2 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC080CF0238C8D850044C5D2 /* StoreKit.framework */; }; + DC080CF2238C8DF70044C5D2 /* OCLicenseAppStoreItem.h in Headers */ = {isa = PBXBuildFile; fileRef = DC080CE7238BD71F0044C5D2 /* OCLicenseAppStoreItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC080CF3238C92480044C5D2 /* OCLicenseAppStoreItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DC080CE8238BD71F0044C5D2 /* OCLicenseAppStoreItem.m */; }; DC0B379420514E4700189B9A /* ServerListBookmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */; }; DC0B37972051681600189B9A /* ThemeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0B37962051681600189B9A /* ThemeButton.swift */; }; DC136582208223F000FC0F60 /* OCBookmark+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */; }; @@ -210,6 +215,8 @@ DC20DE5C21C01A3D0096000B /* ownCloudMocking.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; DC20DE6A21C01B210096000B /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; DC20DE6B21C01B210096000B /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; }; + DC23D1D9238F390A00423F62 /* OCLicenseAppStoreReceipt.m in Sources */ = {isa = PBXBuildFile; fileRef = DC23D1D7238F390200423F62 /* OCLicenseAppStoreReceipt.m */; }; + DC23D1DA238F391200423F62 /* OCLicenseAppStoreReceipt.h in Headers */ = {isa = PBXBuildFile; fileRef = DC23D1D6238F390200423F62 /* OCLicenseAppStoreReceipt.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC243BFF2317B446004FBB5C /* ThemeWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC243BF92317B446004FBB5C /* ThemeWindow.swift */; }; DC248C67213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC248C66213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift */; }; DC2565EE225F5A1900828AA5 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC2565E8225F5A1900828AA5 /* UserNotifications.framework */; }; @@ -260,6 +267,13 @@ DC63208521FCEBE9007EC0A8 /* ClientActivityCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC63208421FCEBE9007EC0A8 /* ClientActivityCell.swift */; }; DC63208721FCEE5D007EC0A8 /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC63208621FCEE5D007EC0A8 /* ProgressView.swift */; }; DC6428D02081406800493A01 /* CollapsibleProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6428CF2081406800493A01 /* CollapsibleProgressBar.swift */; }; + DC66F39C239659C000CF4812 /* OCASN1.h in Headers */ = {isa = PBXBuildFile; fileRef = DC66F39A239659C000CF4812 /* OCASN1.h */; }; + DC66F39D239659C000CF4812 /* OCASN1.m in Sources */ = {isa = PBXBuildFile; fileRef = DC66F39B239659C000CF4812 /* OCASN1.m */; }; + DC66F3A523965A1400CF4812 /* NSDate+RFC3339.h in Headers */ = {isa = PBXBuildFile; fileRef = DC66F3A323965A1400CF4812 /* NSDate+RFC3339.h */; }; + DC66F3A623965A1400CF4812 /* NSDate+RFC3339.m in Sources */ = {isa = PBXBuildFile; fileRef = DC66F3A423965A1400CF4812 /* NSDate+RFC3339.m */; }; + DC66F3AB23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h in Headers */ = {isa = PBXBuildFile; fileRef = DC66F3A923965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC66F3AC23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m in Sources */ = {isa = PBXBuildFile; fileRef = DC66F3AA23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m */; }; + DC66F3AD2396630100CF4812 /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = DC66F3A823965BF400CF4812 /* AppleIncRootCertificate.cer */; }; DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC680575212DF548006C3B1F /* CertificateManagementViewController.swift */; }; DC68057A212EAB5E006C3B1F /* ThemeCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */; }; DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6CF7FA219446050013B9F9 /* LogSettingsViewController.swift */; }; @@ -283,6 +297,8 @@ DC85572D20513B8C00189B9A /* ServerListTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC85572B20513B8C00189B9A /* ServerListTableViewController.xib */; }; DC869A592153B1F60088977E /* OCMockingManager+SwiftTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC869A582153B1F60088977E /* OCMockingManager+SwiftTools.swift */; }; DC89C45D20860B5D0044BCAE /* ProgressSummarizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45C20860B5D0044BCAE /* ProgressSummarizer.swift */; }; + DC8EB26C23927FDD009148F9 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2347445E2076138000859C93 /* openssl.framework */; }; + DC8EB271239308E5009148F9 /* LicenseOffersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8EB270239308E5009148F9 /* LicenseOffersViewController.swift */; }; DC98BBCB20FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.m in Sources */ = {isa = PBXBuildFile; fileRef = DC98BBCA20FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.m */; }; DC98BBD420FF824600F4ED3E /* FileProviderEnumeratorObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DC98BBD320FF824600F4ED3E /* FileProviderEnumeratorObserver.m */; }; DC9BFBB320A19AF4007064B5 /* doc in Resources */ = {isa = PBXBuildFile; fileRef = DC9BFBB220A19AF3007064B5 /* doc */; }; @@ -298,7 +314,6 @@ DCB504DD221EF07F007638BE /* status-flash.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DCB504D7221EF07E007638BE /* status-flash.tvg */; }; DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC085502293ED52008CC05C /* DisplaySettingsSection.swift */; }; DCC085652293F1FD008CC05C /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; - DCC0856C2293F1FD008CC05C /* ownCloudAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC0856B2293F1FD008CC05C /* ownCloudAppTests.m */; }; DCC0856E2293F1FD008CC05C /* ownCloudApp.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC0855E2293F1FD008CC05C /* ownCloudApp.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC085712293F1FD008CC05C /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; DCC085722293F1FD008CC05C /* ownCloudApp.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -313,8 +328,29 @@ DCC6565B20C9B7E400110A97 /* DocumentActionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC6565A20C9B7E400110A97 /* DocumentActionViewController.m */; }; DCC6565E20C9B7E400110A97 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCC6565C20C9B7E400110A97 /* MainInterface.storyboard */; }; DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DCC6564620C9B7E300110A97 /* ownCloud File Provider.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DCC8535823CE1236007BA3EB /* LicenseInAppProductListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC8535723CE1236007BA3EB /* LicenseInAppProductListViewController.swift */; }; + DCC8536023CE1AF8007BA3EB /* PurchasesSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC8535F23CE1AF8007BA3EB /* PurchasesSettingsSection.swift */; }; + DCD1300A23A191C000255779 /* LicenseOfferButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD1300923A191C000255779 /* LicenseOfferButton.swift */; }; + DCD1301123A23F4E00255779 /* OCLicenseManager+AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD1301023A23F4E00255779 /* OCLicenseManager+AppStore.swift */; }; DCD2D40622F06ECA0071FB8F /* StorageSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD2D40522F06ECA0071FB8F /* StorageSettingsSection.swift */; }; + DCD8109A23984AF2003B0053 /* OCLicenseDuration.h in Headers */ = {isa = PBXBuildFile; fileRef = DCD810922398492C003B0053 /* OCLicenseDuration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCD8109B23984AF6003B0053 /* OCLicenseDuration.m in Sources */ = {isa = PBXBuildFile; fileRef = DCD810932398492C003B0053 /* OCLicenseDuration.m */; }; + DCD9B87B2379612B00691929 /* OCLicenseManager+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = DCD9B873237960E600691929 /* OCLicenseManager+Internal.h */; }; + DCDC0ACF23CD186400DFE36D /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; + DCDC0AD123CD18D200DFE36D /* OCLicenseManager+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDC0AD023CD18D200DFE36D /* OCLicenseManager+Setup.swift */; }; + DCDC0AD323CD1E3200DFE36D /* IntentSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDC0AD223CD1E3200DFE36D /* IntentSettings.swift */; }; + DCDC208C239912DC003CFF5B /* OCLicenseTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = DCDC208A239912DC003CFF5B /* OCLicenseTransaction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCDC208D239912DC003CFF5B /* OCLicenseTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = DCDC208B239912DC003CFF5B /* OCLicenseTransaction.m */; }; + DCDC208F23994DFB003CFF5B /* LicenseTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDC208E23994DFB003CFF5B /* LicenseTransactionsViewController.swift */; }; + DCDC209C2399A4CF003CFF5B /* LicenseRequirements.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDC209B2399A4CF003CFF5B /* LicenseRequirements.swift */; }; + DCDC20A12399A715003CFF5B /* OCCore+LicenseEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = DCDC209F2399A715003CFF5B /* OCCore+LicenseEnvironment.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCDC20A22399A715003CFF5B /* OCCore+LicenseEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = DCDC20A02399A715003CFF5B /* OCCore+LicenseEnvironment.m */; }; + DCDC20AB2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DCDC20A92399A8CF003CFF5B /* OCLicenseEnterpriseProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCDC20AC2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCDC20AA2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m */; }; + DCDF58B323CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDF58B223CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift */; }; DCE0275E21F1DF7E00F2544E /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; settings = {ATTRIBUTES = (Required, ); }; }; + DCE0FC4723E42ACB0037B4AD /* Localized.strings in Resources */ = {isa = PBXBuildFile; fileRef = DCE0FC4923E42ACB0037B4AD /* Localized.strings */; }; + DCE442CE2387452000940A6D /* LicensingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC0856B2293F1FD008CC05C /* LicensingTests.m */; }; DCE5E8A12080D781005F60CE /* video.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DCE5E88C2080D77E005F60CE /* video.tvg */; }; DCE5E8A22080D781005F60CE /* folder-external.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DCE5E88D2080D77E005F60CE /* folder-external.tvg */; }; DCE5E8A32080D781005F60CE /* file.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DCE5E88E2080D77E005F60CE /* file.tvg */; }; @@ -339,6 +375,7 @@ DCE5E8B82080D8D9005F60CE /* OCItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE5E8B72080D8D9005F60CE /* OCItem+Extension.swift */; }; DCE974B2207E3AF80069FC2B /* ThemeNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE974B1207E3AF80069FC2B /* ThemeNavigationController.swift */; }; DCE974BC207EACA60069FC2B /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */; }; + DCEE1C9C23A0EADD00FE8D98 /* LicenseOfferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCEE1C9B23A0EADD00FE8D98 /* LicenseOfferView.swift */; }; DCF4F17920519F8C00189B9A /* StaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F17820519F8C00189B9A /* StaticTableViewController.swift */; }; DCF4F17B20519F9D00189B9A /* StaticTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F17A20519F9D00189B9A /* StaticTableViewSection.swift */; }; DCF4F17F2051A0D000189B9A /* StaticTableViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF4F17E2051A0D000189B9A /* StaticTableViewRow.swift */; }; @@ -346,6 +383,23 @@ DCFBAD0C21BE67A100943F76 /* ownCloudUI.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DCFED972208095E200A2D984 /* ClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFED971208095E200A2D984 /* ClientItemCell.swift */; }; DCFED9BA20809B8900A2D984 /* ThemeTVGResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFED9B920809B8900A2D984 /* ThemeTVGResource.swift */; }; + DCFEFE2A236876BD009A142F /* OCLicenseManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE28236876BD009A142F /* OCLicenseManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE2B236876BD009A142F /* OCLicenseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE29236876BD009A142F /* OCLicenseManager.m */; }; + DCFEFE2E236876D4009A142F /* OCLicenseProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE2C236876D4009A142F /* OCLicenseProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE2F236876D4009A142F /* OCLicenseProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE2D236876D4009A142F /* OCLicenseProvider.m */; }; + DCFEFE39236877A7009A142F /* OCLicenseFeature.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE37236877A7009A142F /* OCLicenseFeature.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE3A236877A7009A142F /* OCLicenseFeature.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE38236877A7009A142F /* OCLicenseFeature.m */; }; + DCFEFE3D236877B7009A142F /* OCLicenseProduct.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE3B236877B7009A142F /* OCLicenseProduct.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE3E236877B7009A142F /* OCLicenseProduct.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE3C236877B7009A142F /* OCLicenseProduct.m */; }; + DCFEFE4523687BF5009A142F /* OCLicenseTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE4323687BF5009A142F /* OCLicenseTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE4923687C83009A142F /* OCLicenseEntitlement.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE4723687C83009A142F /* OCLicenseEntitlement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE4A23687C83009A142F /* OCLicenseEntitlement.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE4823687C83009A142F /* OCLicenseEntitlement.m */; }; + DCFEFE4F236880B5009A142F /* OCLicenseOffer.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE4D236880B5009A142F /* OCLicenseOffer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE50236880B5009A142F /* OCLicenseOffer.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE4E236880B5009A142F /* OCLicenseOffer.m */; }; + DCFEFE972368D099009A142F /* OCLicenseEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE952368D099009A142F /* OCLicenseEnvironment.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE982368D099009A142F /* OCLicenseEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE962368D099009A142F /* OCLicenseEnvironment.m */; }; + DCFEFE9C2368D7FA009A142F /* OCLicenseObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE9A2368D7FA009A142F /* OCLicenseObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DCFEFE9D2368D7FA009A142F /* OCLicenseObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE9B2368D7FA009A142F /* OCLicenseObserver.m */; }; EA1D571C6B1E95925C459228 /* EarlGrey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D9C062DD1E85A838608B0F /* EarlGrey.framework */; }; EA88A55521BFD5BF0055A58F /* DeleteBookmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA88A55421BFD5BF0055A58F /* DeleteBookmarkTests.swift */; }; EA9337E32226DB070054971F /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9337E22226DB070054971F /* SettingsTests.swift */; }; @@ -534,6 +588,13 @@ remoteGlobalIDString = C79800BA1A21CB5300380860; remoteInfo = "PocketSVG (iOS)"; }; + DC8EB26D23927FE7009148F9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DCD344AF205BD0FA00189B9A /* openssl.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = DC30949C205749EA00189B9A; + remoteInfo = openssl; + }; DCC085662293F1FD008CC05C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 233BDE94204FEFE500C06732 /* Project object */; @@ -583,6 +644,13 @@ remoteGlobalIDString = DCC6564520C9B7E300110A97; remoteInfo = "ownCloud File Provider"; }; + DCDC0ACD23CD185F00DFE36D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 233BDE94204FEFE500C06732 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DCC0855B2293F1FD008CC05C; + remoteInfo = ownCloudApp; + }; DCE93FF221FCA434000E14F2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DCE93FEE21FCA434000E14F2 /* libzip.xcodeproj */; @@ -888,6 +956,11 @@ DC018F8220A0F56300135198 /* UIView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extension.swift"; sourceTree = ""; }; DC018F8B20A1060A00135198 /* ProgressHUDViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressHUDViewController.swift; sourceTree = ""; }; DC01CDCB212EDDF600FC8E38 /* TextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewController.swift; sourceTree = ""; }; + DC080CE2238AE3ED0044C5D2 /* OCLicenseAppStoreProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseAppStoreProvider.h; sourceTree = ""; }; + DC080CE3238AE3ED0044C5D2 /* OCLicenseAppStoreProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseAppStoreProvider.m; sourceTree = ""; }; + DC080CE7238BD71F0044C5D2 /* OCLicenseAppStoreItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseAppStoreItem.h; sourceTree = ""; }; + DC080CE8238BD71F0044C5D2 /* OCLicenseAppStoreItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseAppStoreItem.m; sourceTree = ""; }; + DC080CF0238C8D850044C5D2 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListBookmarkCell.swift; sourceTree = ""; }; DC0B37952051541C00189B9A /* ownCloud.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ownCloud.entitlements; sourceTree = ""; }; DC0B37962051681600189B9A /* ThemeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeButton.swift; sourceTree = ""; }; @@ -898,6 +971,8 @@ DC1B2705209CF0D3004715E1 /* CertificateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificateViewController.swift; sourceTree = ""; }; DC1B2706209CF0D3004715E1 /* ConnectionIssueViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionIssueViewController.swift; sourceTree = ""; }; DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewController.swift; sourceTree = ""; }; + DC23D1D6238F390200423F62 /* OCLicenseAppStoreReceipt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseAppStoreReceipt.h; sourceTree = ""; }; + DC23D1D7238F390200423F62 /* OCLicenseAppStoreReceipt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseAppStoreReceipt.m; sourceTree = ""; }; DC243BF92317B446004FBB5C /* ThemeWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeWindow.swift; sourceTree = ""; }; DC248C66213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Extension.swift"; sourceTree = ""; }; DC2565E8225F5A1900828AA5 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; @@ -947,6 +1022,13 @@ DC63208421FCEBE9007EC0A8 /* ClientActivityCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientActivityCell.swift; sourceTree = ""; }; DC63208621FCEE5D007EC0A8 /* ProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; DC6428CF2081406800493A01 /* CollapsibleProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleProgressBar.swift; sourceTree = ""; }; + DC66F39A239659C000CF4812 /* OCASN1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCASN1.h; sourceTree = ""; }; + DC66F39B239659C000CF4812 /* OCASN1.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCASN1.m; sourceTree = ""; }; + DC66F3A323965A1400CF4812 /* NSDate+RFC3339.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDate+RFC3339.h"; sourceTree = ""; }; + DC66F3A423965A1400CF4812 /* NSDate+RFC3339.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDate+RFC3339.m"; sourceTree = ""; }; + DC66F3A823965BF400CF4812 /* AppleIncRootCertificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AppleIncRootCertificate.cer; sourceTree = ""; }; + DC66F3A923965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseAppStoreReceiptInAppPurchase.h; sourceTree = ""; }; + DC66F3AA23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseAppStoreReceiptInAppPurchase.m; sourceTree = ""; }; DC680575212DF548006C3B1F /* CertificateManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateManagementViewController.swift; sourceTree = ""; }; DC680579212EAB5E006C3B1F /* ThemeCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeCertificateViewController.swift; sourceTree = ""; }; DC6CF7FA219446050013B9F9 /* LogSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogSettingsViewController.swift; sourceTree = ""; }; @@ -970,6 +1052,7 @@ DC85572B20513B8C00189B9A /* ServerListTableViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ServerListTableViewController.xib; sourceTree = ""; }; DC869A582153B1F60088977E /* OCMockingManager+SwiftTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCMockingManager+SwiftTools.swift"; sourceTree = ""; }; DC89C45C20860B5D0044BCAE /* ProgressSummarizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSummarizer.swift; sourceTree = ""; }; + DC8EB270239308E5009148F9 /* LicenseOffersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseOffersViewController.swift; sourceTree = ""; }; DC98BBC920FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSNumber+OCSyncAnchorData.h"; sourceTree = ""; }; DC98BBCA20FF815C00F4ED3E /* NSNumber+OCSyncAnchorData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSNumber+OCSyncAnchorData.m"; sourceTree = ""; }; DC98BBD220FF824600F4ED3E /* FileProviderEnumeratorObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileProviderEnumeratorObserver.h; sourceTree = ""; }; @@ -990,7 +1073,7 @@ DCC0855E2293F1FD008CC05C /* ownCloudApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ownCloudApp.h; sourceTree = ""; }; DCC0855F2293F1FD008CC05C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DCC085642293F1FD008CC05C /* ownCloudAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ownCloudAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DCC0856B2293F1FD008CC05C /* ownCloudAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ownCloudAppTests.m; sourceTree = ""; }; + DCC0856B2293F1FD008CC05C /* LicensingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LicensingTests.m; sourceTree = ""; }; DCC0856D2293F1FD008CC05C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DCC0857C2293F2D7008CC05C /* DisplaySettings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DisplaySettings.h; sourceTree = ""; }; DCC0857D2293F2D7008CC05C /* DisplaySettings.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DisplaySettings.m; sourceTree = ""; }; @@ -1008,9 +1091,28 @@ DCC6565A20C9B7E400110A97 /* DocumentActionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DocumentActionViewController.m; sourceTree = ""; }; DCC6565D20C9B7E400110A97 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; DCC6565F20C9B7E400110A97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DCC8535723CE1236007BA3EB /* LicenseInAppProductListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseInAppProductListViewController.swift; sourceTree = ""; }; + DCC8535F23CE1AF8007BA3EB /* PurchasesSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesSettingsSection.swift; sourceTree = ""; }; + DCD1300923A191C000255779 /* LicenseOfferButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseOfferButton.swift; sourceTree = ""; }; + DCD1301023A23F4E00255779 /* OCLicenseManager+AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCLicenseManager+AppStore.swift"; sourceTree = ""; }; DCD2D40522F06ECA0071FB8F /* StorageSettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettingsSection.swift; sourceTree = ""; }; DCD344A5205BD0C000189B9A /* openssl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DCD344AF205BD0FA00189B9A /* openssl.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = openssl.xcodeproj; path = "ios-sdk/ownCloudUI/openssl/framework/openssl.xcodeproj"; sourceTree = ""; }; + DCD810922398492C003B0053 /* OCLicenseDuration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseDuration.h; sourceTree = ""; }; + DCD810932398492C003B0053 /* OCLicenseDuration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseDuration.m; sourceTree = ""; }; + DCD9B873237960E600691929 /* OCLicenseManager+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCLicenseManager+Internal.h"; sourceTree = ""; }; + DCDC0AD023CD18D200DFE36D /* OCLicenseManager+Setup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCLicenseManager+Setup.swift"; sourceTree = ""; }; + DCDC0AD223CD1E3200DFE36D /* IntentSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentSettings.swift; sourceTree = ""; }; + DCDC208A239912DC003CFF5B /* OCLicenseTransaction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseTransaction.h; sourceTree = ""; }; + DCDC208B239912DC003CFF5B /* OCLicenseTransaction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseTransaction.m; sourceTree = ""; }; + DCDC208E23994DFB003CFF5B /* LicenseTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseTransactionsViewController.swift; sourceTree = ""; }; + DCDC209B2399A4CF003CFF5B /* LicenseRequirements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseRequirements.swift; sourceTree = ""; }; + DCDC209F2399A715003CFF5B /* OCCore+LicenseEnvironment.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCCore+LicenseEnvironment.h"; sourceTree = ""; }; + DCDC20A02399A715003CFF5B /* OCCore+LicenseEnvironment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCCore+LicenseEnvironment.m"; sourceTree = ""; }; + DCDC20A92399A8CF003CFF5B /* OCLicenseEnterpriseProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseEnterpriseProvider.h; sourceTree = ""; }; + DCDC20AA2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseEnterpriseProvider.m; sourceTree = ""; }; + DCDF58B223CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseInAppPurchaseFeatureView.swift; sourceTree = ""; }; + DCE0FC4823E42ACB0037B4AD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localized.strings; sourceTree = ""; }; DCE5E88C2080D77E005F60CE /* video.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = video.tvg; path = "img/filetypes-tvg/video.tvg"; sourceTree = SOURCE_ROOT; }; DCE5E88D2080D77E005F60CE /* folder-external.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "folder-external.tvg"; path = "img/filetypes-tvg/folder-external.tvg"; sourceTree = SOURCE_ROOT; }; DCE5E88E2080D77E005F60CE /* file.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = file.tvg; path = "img/filetypes-tvg/file.tvg"; sourceTree = SOURCE_ROOT; }; @@ -1036,12 +1138,31 @@ DCE93FEE21FCA434000E14F2 /* libzip.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = libzip.xcodeproj; path = external/libzip/libzip.xcodeproj; sourceTree = SOURCE_ROOT; }; DCE974B1207E3AF80069FC2B /* ThemeNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeNavigationController.swift; sourceTree = ""; }; DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; + DCEE1C9B23A0EADD00FE8D98 /* LicenseOfferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseOfferView.swift; sourceTree = ""; }; DCF4F17820519F8C00189B9A /* StaticTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewController.swift; sourceTree = ""; }; DCF4F17A20519F9D00189B9A /* StaticTableViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewSection.swift; sourceTree = ""; }; DCF4F17E2051A0D000189B9A /* StaticTableViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewRow.swift; sourceTree = ""; }; DCF4F18A2052BA4C00189B9A /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; DCFED971208095E200A2D984 /* ClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientItemCell.swift; sourceTree = ""; }; DCFED9B920809B8900A2D984 /* ThemeTVGResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTVGResource.swift; sourceTree = ""; }; + DCFEFE28236876BD009A142F /* OCLicenseManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseManager.h; sourceTree = ""; }; + DCFEFE29236876BD009A142F /* OCLicenseManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseManager.m; sourceTree = ""; }; + DCFEFE2C236876D4009A142F /* OCLicenseProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseProvider.h; sourceTree = ""; }; + DCFEFE2D236876D4009A142F /* OCLicenseProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseProvider.m; sourceTree = ""; }; + DCFEFE37236877A7009A142F /* OCLicenseFeature.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseFeature.h; sourceTree = ""; }; + DCFEFE38236877A7009A142F /* OCLicenseFeature.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseFeature.m; sourceTree = ""; }; + DCFEFE3B236877B7009A142F /* OCLicenseProduct.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseProduct.h; sourceTree = ""; }; + DCFEFE3C236877B7009A142F /* OCLicenseProduct.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseProduct.m; sourceTree = ""; }; + DCFEFE422368782F009A142F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DCFEFE4323687BF5009A142F /* OCLicenseTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseTypes.h; sourceTree = ""; }; + DCFEFE4723687C83009A142F /* OCLicenseEntitlement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseEntitlement.h; sourceTree = ""; }; + DCFEFE4823687C83009A142F /* OCLicenseEntitlement.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseEntitlement.m; sourceTree = ""; }; + DCFEFE4D236880B5009A142F /* OCLicenseOffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseOffer.h; sourceTree = ""; }; + DCFEFE4E236880B5009A142F /* OCLicenseOffer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseOffer.m; sourceTree = ""; }; + DCFEFE952368D099009A142F /* OCLicenseEnvironment.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseEnvironment.h; sourceTree = ""; }; + DCFEFE962368D099009A142F /* OCLicenseEnvironment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseEnvironment.m; sourceTree = ""; }; + DCFEFE9A2368D7FA009A142F /* OCLicenseObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseObserver.h; sourceTree = ""; }; + DCFEFE9B2368D7FA009A142F /* OCLicenseObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseObserver.m; sourceTree = ""; }; DF01EC88E3D07CDA6F366E32 /* Pods-ownCloud Screenshots Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.release.xcconfig"; sourceTree = ""; }; EA88A55421BFD5BF0055A58F /* DeleteBookmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteBookmarkTests.swift; sourceTree = ""; }; EA9337E22226DB070054971F /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; @@ -1055,6 +1176,7 @@ DC7DBA1F207F59F800E7337D /* PocketSVG.framework in Frameworks */, DC3BE0D72077BC5D002A0AC0 /* openssl.framework in Frameworks */, DC3BE0D82077BC5D002A0AC0 /* ownCloudSDK.framework in Frameworks */, + DC080CF1238C8D850044C5D2 /* StoreKit.framework in Frameworks */, DCC085712293F1FD008CC05C /* ownCloudApp.framework in Frameworks */, 394A0B0022EEFC2C00603813 /* ownCloudAppShared.framework in Frameworks */, DCE0275E21F1DF7E00F2544E /* ownCloudUI.framework in Frameworks */, @@ -1077,6 +1199,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DCDC0ACF23CD186400DFE36D /* ownCloudApp.framework in Frameworks */, 394A0B0A22EEFCF500603813 /* ownCloudSDK.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1112,6 +1235,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DC8EB26C23927FDD009148F9 /* openssl.framework in Frameworks */, DC774E6822F44F6A000B11A1 /* libzip.framework in Frameworks */, DCC0857B2293F29F008CC05C /* ownCloudSDK.framework in Frameworks */, ); @@ -1205,6 +1329,7 @@ 3961281522F8730A0087BD3A /* SceneDelegate.swift */, DCF4F1612051925A00189B9A /* Bookmarks */, DC29F09122974F8000F77349 /* FileLists */, + DC8EB26F239308C3009148F9 /* Licensing */, 4C51727422DE04BD001BC97F /* Tasks */, DC3BE0DB2077CC13002A0AC0 /* Client */, DC7DF17C205140F400189B9A /* Server List */, @@ -1384,6 +1509,7 @@ 395E16FB22F0691F00DE89A1 /* Intent */ = { isa = PBXGroup; children = ( + DCDC0AD223CD1E3200DFE36D /* IntentSettings.swift */, 399A4C012317D1BA0027DDD6 /* Extensions */, 39057AAA233BA7A60008E6C0 /* Intents.intentdefinition */, 3940C4EF2326985B008227AE /* GetAccountIntentHandler.swift */, @@ -1439,6 +1565,7 @@ isa = PBXGroup; children = ( 399A4C022317D1ED0027DDD6 /* OCBookmarkManager+Extension.swift */, + DCDC0AD023CD18D200DFE36D /* OCLicenseManager+Setup.swift */, ); path = Extensions; sourceTree = ""; @@ -1628,6 +1755,27 @@ path = Tools; sourceTree = ""; }; + DC080CDC238AE3D00044C5D2 /* App Store */ = { + isa = PBXGroup; + children = ( + DC080CE3238AE3ED0044C5D2 /* OCLicenseAppStoreProvider.m */, + DC080CE2238AE3ED0044C5D2 /* OCLicenseAppStoreProvider.h */, + DC23D1D0238F38DF00423F62 /* Receipt */, + DC080CEF238BEBC00044C5D2 /* Items */, + DC66F3A723965BE300CF4812 /* Parser Support */, + ); + path = "App Store"; + sourceTree = ""; + }; + DC080CEF238BEBC00044C5D2 /* Items */ = { + isa = PBXGroup; + children = ( + DC080CE8238BD71F0044C5D2 /* OCLicenseAppStoreItem.m */, + DC080CE7238BD71F0044C5D2 /* OCLicenseAppStoreItem.h */, + ); + path = Items; + sourceTree = ""; + }; DC1B26FD209CF0D2004715E1 /* Issues Animators */ = { isa = PBXGroup; children = ( @@ -1646,6 +1794,17 @@ path = "Issues Subclasses"; sourceTree = ""; }; + DC23D1D0238F38DF00423F62 /* Receipt */ = { + isa = PBXGroup; + children = ( + DC23D1D7238F390200423F62 /* OCLicenseAppStoreReceipt.m */, + DC23D1D6238F390200423F62 /* OCLicenseAppStoreReceipt.h */, + DC66F3AA23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m */, + DC66F3A923965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h */, + ); + path = Receipt; + sourceTree = ""; + }; DC255E432319AD13007279B1 /* Scanner */ = { isa = PBXGroup; children = ( @@ -1731,6 +1890,18 @@ path = Theming; sourceTree = ""; }; + DC66F3A723965BE300CF4812 /* Parser Support */ = { + isa = PBXGroup; + children = ( + DC66F3A823965BF400CF4812 /* AppleIncRootCertificate.cer */, + DC66F39B239659C000CF4812 /* OCASN1.m */, + DC66F39A239659C000CF4812 /* OCASN1.h */, + DC66F3A423965A1400CF4812 /* NSDate+RFC3339.m */, + DC66F3A323965A1400CF4812 /* NSDate+RFC3339.h */, + ); + path = "Parser Support"; + sourceTree = ""; + }; DC68056B212DF506006C3B1F /* Certificate Management */ = { isa = PBXGroup; children = ( @@ -1770,6 +1941,7 @@ isa = PBXGroup; children = ( DCC0855F2293F1FD008CC05C /* Info.plist */, + DCE0FC4923E42ACB0037B4AD /* Localized.strings */, ); path = Resources; sourceTree = ""; @@ -1875,6 +2047,7 @@ DC85573220513CC700189B9A /* Frameworks */ = { isa = PBXGroup; children = ( + DC080CF0238C8D850044C5D2 /* StoreKit.framework */, DC2565E8225F5A1900828AA5 /* UserNotifications.framework */, DC27A19020CAA0BA008ACB6C /* MobileCoreServices.framework */, DCD344A5205BD0C000189B9A /* openssl.framework */, @@ -1895,6 +2068,18 @@ path = Progress; sourceTree = ""; }; + DC8EB26F239308C3009148F9 /* Licensing */ = { + isa = PBXGroup; + children = ( + DCDC209B2399A4CF003CFF5B /* LicenseRequirements.swift */, + DCC8535E23CE123C007BA3EB /* Product List */, + DCD1301923A25D0500255779 /* Transactions */, + DCEE1C9523A0EAA300FE8D98 /* Offers */, + DCD1301A23A25D1A00255779 /* Tools */, + ); + path = Licensing; + sourceTree = ""; + }; DCAEB05821F9FB0F0067E147 /* Tools */ = { isa = PBXGroup; children = ( @@ -1945,6 +2130,7 @@ DCC0855E2293F1FD008CC05C /* ownCloudApp.h */, DC774E5A22F44E2A000B11A1 /* Display Settings */, DCC5E443232654C1002E5B84 /* Foundation Extensions */, + DCFEFE2223687637009A142F /* Licensing */, DC774E5422F44DF6000B11A1 /* SDK Extensions */, DC0030BE2350B1CE00BB8570 /* Tools */, DC774E5B22F44E4A000B11A1 /* ZIP Archive */, @@ -1956,7 +2142,7 @@ DCC0856A2293F1FD008CC05C /* ownCloudAppFrameworkTests */ = { isa = PBXGroup; children = ( - DCC0856B2293F1FD008CC05C /* ownCloudAppTests.m */, + DCC0856B2293F1FD008CC05C /* LicensingTests.m */, DCC0856D2293F1FD008CC05C /* Info.plist */, ); path = ownCloudAppFrameworkTests; @@ -2010,6 +2196,15 @@ path = "ownCloud File ProviderUI"; sourceTree = ""; }; + DCC8535E23CE123C007BA3EB /* Product List */ = { + isa = PBXGroup; + children = ( + DCC8535723CE1236007BA3EB /* LicenseInAppProductListViewController.swift */, + DCDF58B223CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift */, + ); + path = "Product List"; + sourceTree = ""; + }; DCCE54092080175B00067D1D /* TVGs */ = { isa = PBXGroup; children = ( @@ -2044,6 +2239,56 @@ name = TVGs; sourceTree = ""; }; + DCD1301923A25D0500255779 /* Transactions */ = { + isa = PBXGroup; + children = ( + DCDC208E23994DFB003CFF5B /* LicenseTransactionsViewController.swift */, + ); + path = Transactions; + sourceTree = ""; + }; + DCD1301A23A25D1A00255779 /* Tools */ = { + isa = PBXGroup; + children = ( + DCD1301023A23F4E00255779 /* OCLicenseManager+AppStore.swift */, + ); + path = Tools; + sourceTree = ""; + }; + DCDC208923991296003CFF5B /* Transactions */ = { + isa = PBXGroup; + children = ( + DCDC208B239912DC003CFF5B /* OCLicenseTransaction.m */, + DCDC208A239912DC003CFF5B /* OCLicenseTransaction.h */, + ); + path = Transactions; + sourceTree = ""; + }; + DCDC20952399A4AB003CFF5B /* Licensing Controller */ = { + isa = PBXGroup; + children = ( + ); + path = "Licensing Controller"; + sourceTree = ""; + }; + DCDC20A32399A720003CFF5B /* Core Integration */ = { + isa = PBXGroup; + children = ( + DCDC20A02399A715003CFF5B /* OCCore+LicenseEnvironment.m */, + DCDC209F2399A715003CFF5B /* OCCore+LicenseEnvironment.h */, + ); + path = "Core Integration"; + sourceTree = ""; + }; + DCDC20A42399A898003CFF5B /* Enterprise */ = { + isa = PBXGroup; + children = ( + DCDC20AA2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m */, + DCDC20A92399A8CF003CFF5B /* OCLicenseEnterpriseProvider.h */, + ); + path = Enterprise; + sourceTree = ""; + }; DCE5E8B62080D8B8005F60CE /* SDK Extensions */ = { isa = PBXGroup; children = ( @@ -2066,6 +2311,16 @@ name = Products; sourceTree = ""; }; + DCEE1C9523A0EAA300FE8D98 /* Offers */ = { + isa = PBXGroup; + children = ( + DCD1300923A191C000255779 /* LicenseOfferButton.swift */, + DCEE1C9B23A0EADD00FE8D98 /* LicenseOfferView.swift */, + DC8EB270239308E5009148F9 /* LicenseOffersViewController.swift */, + ); + path = Offers; + sourceTree = ""; + }; DCF4F1612051925A00189B9A /* Bookmarks */ = { isa = PBXGroup; children = ( @@ -2079,6 +2334,7 @@ DCF4F1622051927200189B9A /* UI Elements */ = { isa = PBXGroup; children = ( + DCDC20952399A4AB003CFF5B /* Licensing Controller */, DC297959226E4C9200E01BC7 /* Push Presentation Controller */, 2366821521144DCD0045EF72 /* Card Presentation Controller */, DCF4F17820519F8C00189B9A /* StaticTableViewController.swift */, @@ -2109,6 +2365,7 @@ DC6CF7FA219446050013B9F9 /* LogSettingsViewController.swift */, 4C88041722E78D790016CBA9 /* MediaFilesSettings.swift */, 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */, + DCC8535F23CE1AF8007BA3EB /* PurchasesSettingsSection.swift */, 23957A6C209AFFE8003C8537 /* MoreSettingsSection.swift */, 593BAB3F209ADFB900023634 /* Passcode */, 233E0FD72099F11D00C3D8D5 /* SecuritySettingsSection.swift */, @@ -2135,6 +2392,94 @@ path = Tools; sourceTree = ""; }; + DCFEFE2223687637009A142F /* Licensing */ = { + isa = PBXGroup; + children = ( + DCFEFE422368782F009A142F /* README.md */, + DCFEFE4323687BF5009A142F /* OCLicenseTypes.h */, + DCFEFE992368D7E3009A142F /* Manager */, + DCFEFE40236877FF009A142F /* Feature */, + DCFEFE4123687807009A142F /* Product */, + DCFEFE3F236877F6009A142F /* Providers */, + DCFEFE4B23687DB9009A142F /* Entitlement */, + DCFEFE4C2368809D009A142F /* Offer */, + DCDC208923991296003CFF5B /* Transactions */, + DCFEFE8F2368D07D009A142F /* Environment */, + DCDC20A32399A720003CFF5B /* Core Integration */, + ); + path = Licensing; + sourceTree = ""; + }; + DCFEFE3F236877F6009A142F /* Providers */ = { + isa = PBXGroup; + children = ( + DCFEFE2D236876D4009A142F /* OCLicenseProvider.m */, + DCFEFE2C236876D4009A142F /* OCLicenseProvider.h */, + DC080CDC238AE3D00044C5D2 /* App Store */, + DCDC20A42399A898003CFF5B /* Enterprise */, + ); + path = Providers; + sourceTree = ""; + }; + DCFEFE40236877FF009A142F /* Feature */ = { + isa = PBXGroup; + children = ( + DCFEFE38236877A7009A142F /* OCLicenseFeature.m */, + DCFEFE37236877A7009A142F /* OCLicenseFeature.h */, + ); + path = Feature; + sourceTree = ""; + }; + DCFEFE4123687807009A142F /* Product */ = { + isa = PBXGroup; + children = ( + DCFEFE3C236877B7009A142F /* OCLicenseProduct.m */, + DCFEFE3B236877B7009A142F /* OCLicenseProduct.h */, + ); + path = Product; + sourceTree = ""; + }; + DCFEFE4B23687DB9009A142F /* Entitlement */ = { + isa = PBXGroup; + children = ( + DCFEFE4823687C83009A142F /* OCLicenseEntitlement.m */, + DCFEFE4723687C83009A142F /* OCLicenseEntitlement.h */, + ); + path = Entitlement; + sourceTree = ""; + }; + DCFEFE4C2368809D009A142F /* Offer */ = { + isa = PBXGroup; + children = ( + DCFEFE4E236880B5009A142F /* OCLicenseOffer.m */, + DCFEFE4D236880B5009A142F /* OCLicenseOffer.h */, + DCD810932398492C003B0053 /* OCLicenseDuration.m */, + DCD810922398492C003B0053 /* OCLicenseDuration.h */, + ); + path = Offer; + sourceTree = ""; + }; + DCFEFE8F2368D07D009A142F /* Environment */ = { + isa = PBXGroup; + children = ( + DCFEFE962368D099009A142F /* OCLicenseEnvironment.m */, + DCFEFE952368D099009A142F /* OCLicenseEnvironment.h */, + ); + path = Environment; + sourceTree = ""; + }; + DCFEFE992368D7E3009A142F /* Manager */ = { + isa = PBXGroup; + children = ( + DCFEFE29236876BD009A142F /* OCLicenseManager.m */, + DCFEFE28236876BD009A142F /* OCLicenseManager.h */, + DCD9B873237960E600691929 /* OCLicenseManager+Internal.h */, + DCFEFE9B2368D7FA009A142F /* OCLicenseObserver.m */, + DCFEFE9A2368D7FA009A142F /* OCLicenseObserver.h */, + ); + path = Manager; + sourceTree = ""; + }; EA9337DC2226DAE00054971F /* Settings */ = { isa = PBXGroup; children = ( @@ -2158,12 +2503,32 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + DCDC20AB2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.h in Headers */, + DCD8109A23984AF2003B0053 /* OCLicenseDuration.h in Headers */, + DCFEFE2A236876BD009A142F /* OCLicenseManager.h in Headers */, + DCFEFE4923687C83009A142F /* OCLicenseEntitlement.h in Headers */, + DCFEFE39236877A7009A142F /* OCLicenseFeature.h in Headers */, + DC23D1DA238F391200423F62 /* OCLicenseAppStoreReceipt.h in Headers */, DC774E6022F44E57000B11A1 /* ZIPArchive.h in Headers */, + DCFEFE9C2368D7FA009A142F /* OCLicenseObserver.h in Headers */, + DCFEFE2E236876D4009A142F /* OCLicenseProvider.h in Headers */, DCC085802293F490008CC05C /* DisplaySettings.h in Headers */, + DCFEFE3D236877B7009A142F /* OCLicenseProduct.h in Headers */, + DCFEFE4523687BF5009A142F /* OCLicenseTypes.h in Headers */, + DC66F39C239659C000CF4812 /* OCASN1.h in Headers */, + DC080CF2238C8DF70044C5D2 /* OCLicenseAppStoreItem.h in Headers */, + DCD9B87B2379612B00691929 /* OCLicenseManager+Internal.h in Headers */, DC774E6322F44E6D000B11A1 /* OCCore+BundleImport.h in Headers */, + DC080CE6238AE3F40044C5D2 /* OCLicenseAppStoreProvider.h in Headers */, + DCDC208C239912DC003CFF5B /* OCLicenseTransaction.h in Headers */, + DCDC20A12399A715003CFF5B /* OCCore+LicenseEnvironment.h in Headers */, + DCFEFE4F236880B5009A142F /* OCLicenseOffer.h in Headers */, DCC0856E2293F1FD008CC05C /* ownCloudApp.h in Headers */, + DC66F3A523965A1400CF4812 /* NSDate+RFC3339.h in Headers */, DC0030C22350B1CE00BB8570 /* NSData+Encoding.h in Headers */, DCC5E4472326564F002E5B84 /* NSObject+AnnotatedProperties.h in Headers */, + DC66F3AB23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h in Headers */, + DCFEFE972368D099009A142F /* OCLicenseEnvironment.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2239,6 +2604,7 @@ buildRules = ( ); dependencies = ( + DCDC0ACE23CD185F00DFE36D /* PBXTargetDependency */, 394A0B0C22EEFD2800603813 /* PBXTargetDependency */, ); name = ownCloudAppShared; @@ -2315,6 +2681,7 @@ buildRules = ( ); dependencies = ( + DC8EB26E23927FE7009148F9 /* PBXTargetDependency */, DC774E6722F44F65000B11A1 /* PBXTargetDependency */, DCC0857A2293F296008CC05C /* PBXTargetDependency */, ); @@ -2418,7 +2785,7 @@ }; 39A7137F22E79C6700089423 = { CreatedOnToolsVersion = 11.0; - ProvisioningStyle = Automatic; + ProvisioningStyle = Manual; }; 59056CA922414F3C00A18A22 = { CreatedOnToolsVersion = 10.1; @@ -2617,6 +2984,7 @@ DCB504DD221EF07F007638BE /* status-flash.tvg in Resources */, DCE5E8B42080D781005F60CE /* folder-shared.tvg in Resources */, DCE5E8A42080D781005F60CE /* audio.tvg in Resources */, + DCE0FC4723E42ACB0037B4AD /* Localized.strings in Resources */, 59D4895220C83F2E00369C2E /* InfoPlist.strings in Resources */, DC3393A822E0C4ED00DD3DA4 /* icon-available-offline.tvg in Resources */, 6EB8EDC52114358400C2BF44 /* folder-create.tvg in Resources */, @@ -2680,6 +3048,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC66F3AD2396630100CF4812 /* AppleIncRootCertificate.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2862,12 +3231,14 @@ DC33939D22E076E300DD3DA4 /* MakeUnavailableOfflineAction.swift in Sources */, 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */, 6E4F1734217749910049A71B /* ImageDisplayViewController.swift in Sources */, + DC8EB271239308E5009148F9 /* LicenseOffersViewController.swift in Sources */, 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */, DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */, 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */, 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */, DC82D6FA23171339001551C5 /* ScanAction.swift in Sources */, 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */, + DCDC209C2399A4CF003CFF5B /* LicenseRequirements.swift in Sources */, DC018F8C20A1060A00135198 /* ProgressHUDViewController.swift in Sources */, 4CB8ADE922DF6DE200F1FEBC /* AVAsset+Extension.swift in Sources */, 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */, @@ -2880,6 +3251,7 @@ 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */, 39A513AC22674E56002CF1AA /* OCCore+Extension.swift in Sources */, DC018F8320A0F56300135198 /* UIView+Extension.swift in Sources */, + DCDC208F23994DFB003CFF5B /* LicenseTransactionsViewController.swift in Sources */, 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */, 4CC4A21222FA20AD00AE7E2C /* URL+Extensions.swift in Sources */, DC42244A207CAFAA0006A2A6 /* Theme.swift in Sources */, @@ -2898,6 +3270,7 @@ DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */, DC0030CB2350B75000BB8570 /* ScanViewController.swift in Sources */, 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */, + DCD1300A23A191C000255779 /* LicenseOfferButton.swift in Sources */, 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, DC1B2709209CF0D3004715E1 /* CertificateViewController.swift in Sources */, DC248C67213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift in Sources */, @@ -2970,6 +3343,7 @@ 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, + DCC8535823CE1236007BA3EB /* LicenseInAppProductListViewController.swift in Sources */, 239F1319205A693A0029F186 /* UIColor+Extension.swift in Sources */, 23EC775B2137F3DD0032D4E6 /* OCExtensionType+Extension.swift in Sources */, 4C88041822E78D790016CBA9 /* MediaFilesSettings.swift in Sources */, @@ -2992,6 +3366,7 @@ 3900348223A100D3000D8510 /* UIApplication+Extension.swift in Sources */, 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */, 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */, + DCEE1C9C23A0EADD00FE8D98 /* LicenseOfferView.swift in Sources */, 6E3A104D219D6F0100F90C96 /* DuplicateAction.swift in Sources */, DC1B270A209CF0D3004715E1 /* ConnectionIssueViewController.swift in Sources */, DC0B37972051681600189B9A /* ThemeButton.swift in Sources */, @@ -2999,12 +3374,14 @@ DCF4F17B20519F9D00189B9A /* StaticTableViewSection.swift in Sources */, DC243BFF2317B446004FBB5C /* ThemeWindow.swift in Sources */, 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */, + DCC8536023CE1AF8007BA3EB /* PurchasesSettingsSection.swift in Sources */, DCD2D40622F06ECA0071FB8F /* StorageSettingsSection.swift in Sources */, 39B289A8226F1EE000BE0E11 /* MessageView.swift in Sources */, 4C464BF22187AF1500D30602 /* PDFSearchViewController.swift in Sources */, DC3393A422E0A75C00DD3DA4 /* ClientItemResolvingCell.swift in Sources */, DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */, DC7DBA2B207F71E400E7337D /* VectorImageView.swift in Sources */, + DCD1301123A23F4E00255779 /* OCLicenseManager+AppStore.swift in Sources */, 39DE75CD22F960CF0064C1E2 /* SortMethodTableViewController.swift in Sources */, DCE974BC207EACA60069FC2B /* UIImage+Extension.swift in Sources */, DC1B2707209CF0D3004715E1 /* IssuesDismissalAnimator.swift in Sources */, @@ -3017,6 +3394,7 @@ 4C6B78102226B83300C5F3DB /* PhotoAlbumTableViewController.swift in Sources */, DC3393A222E0A71100DD3DA4 /* ItemPolicyCell.swift in Sources */, 23EC77592137F3DD0032D4E6 /* DisplayExtension.swift in Sources */, + DCDF58B323CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift in Sources */, 39AFC3D8225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift in Sources */, DC3DEC8022B03AE700F3352D /* CardViewController.swift in Sources */, DC297967226E4D3100E01BC7 /* PushPresentationController.swift in Sources */, @@ -3074,6 +3452,7 @@ buildActionMask = 2147483647; files = ( 395E16FA22F03CAF00DE89A1 /* GetFileIntentHandler.swift in Sources */, + DCDC0AD123CD18D200DFE36D /* OCLicenseManager+Setup.swift in Sources */, 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */, 397754E223279EED00119FCB /* OCItem+Extension.swift in Sources */, 39BE385D23435AFE0062A2FE /* String+Extension.swift in Sources */, @@ -3082,6 +3461,7 @@ 39F689AC22F0206900E63429 /* OCBookmark+Extension.swift in Sources */, 395E16FF22F172C900DE89A1 /* CreateFolderIntentHandler.swift in Sources */, 395E16FD22F06A7300DE89A1 /* SaveFileIntentHandler.swift in Sources */, + DCDC0AD323CD1E3200DFE36D /* IntentSettings.swift in Sources */, 39E6DE84233CC39A008DAE04 /* Intents.intentdefinition in Sources */, 3940C4F02326985B008227AE /* GetAccountIntentHandler.swift in Sources */, 39A5C3A1231566D9009D9EE3 /* GetFileInfoIntentHandler.swift in Sources */, @@ -3127,10 +3507,28 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DCFEFE9D2368D7FA009A142F /* OCLicenseObserver.m in Sources */, + DC66F39D239659C000CF4812 /* OCASN1.m in Sources */, + DC66F3A623965A1400CF4812 /* NSDate+RFC3339.m in Sources */, + DCFEFE3E236877B7009A142F /* OCLicenseProduct.m in Sources */, DC774E6422F44E6D000B11A1 /* OCCore+BundleImport.m in Sources */, + DCDC208D239912DC003CFF5B /* OCLicenseTransaction.m in Sources */, + DC23D1D9238F390A00423F62 /* OCLicenseAppStoreReceipt.m in Sources */, + DCFEFE2B236876BD009A142F /* OCLicenseManager.m in Sources */, + DCDC20A22399A715003CFF5B /* OCCore+LicenseEnvironment.m in Sources */, + DCDC20AC2399A8CF003CFF5B /* OCLicenseEnterpriseProvider.m in Sources */, + DCFEFE3A236877A7009A142F /* OCLicenseFeature.m in Sources */, + DCFEFE50236880B5009A142F /* OCLicenseOffer.m in Sources */, DC0030C12350B1CE00BB8570 /* NSData+Encoding.m in Sources */, DC774E5F22F44E57000B11A1 /* ZIPArchive.m in Sources */, + DC080CE5238AE3F40044C5D2 /* OCLicenseAppStoreProvider.m in Sources */, DCC0857F2293F48D008CC05C /* DisplaySettings.m in Sources */, + DCFEFE2F236876D4009A142F /* OCLicenseProvider.m in Sources */, + DCFEFE4A23687C83009A142F /* OCLicenseEntitlement.m in Sources */, + DC66F3AC23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.m in Sources */, + DCD8109B23984AF6003B0053 /* OCLicenseDuration.m in Sources */, + DC080CF3238C92480044C5D2 /* OCLicenseAppStoreItem.m in Sources */, + DCFEFE982368D099009A142F /* OCLicenseEnvironment.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3138,8 +3536,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DCE442CE2387452000940A6D /* LicensingTests.m in Sources */, 39057AA7233BA7A60008E6C0 /* Intents.intentdefinition in Sources */, - DCC0856C2293F1FD008CC05C /* ownCloudAppTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3250,6 +3648,11 @@ name = "PocketSVG (iOS)"; targetProxy = DC7DBA1D207F59F200E7337D /* PBXContainerItemProxy */; }; + DC8EB26E23927FE7009148F9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = openssl; + targetProxy = DC8EB26D23927FE7009148F9 /* PBXContainerItemProxy */; + }; DCC085672293F1FD008CC05C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DCC0855B2293F1FD008CC05C /* ownCloudApp */; @@ -3285,6 +3688,11 @@ target = DCC6564520C9B7E300110A97 /* ownCloud File Provider */; targetProxy = DCC6566320C9B7E400110A97 /* PBXContainerItemProxy */; }; + DCDC0ACE23CD185F00DFE36D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DCC0855B2293F1FD008CC05C /* ownCloudApp */; + targetProxy = DCDC0ACD23CD185F00DFE36D /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -3371,6 +3779,14 @@ name = MainInterface.storyboard; sourceTree = ""; }; + DCE0FC4923E42ACB0037B4AD /* Localized.strings */ = { + isa = PBXVariantGroup; + children = ( + DCE0FC4823E42ACB0037B4AD /* en */, + ); + name = Localized.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -3587,6 +4003,7 @@ 394A0B0322EEFC2C00603813 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -3620,6 +4037,7 @@ 394A0B0422EEFC2C00603813 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_OBJC_WEAK = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; @@ -3652,7 +4070,7 @@ CODE_SIGN_ENTITLEMENTS = "ownCloud Intents/ownCloud Intents.entitlements"; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = 4AP2STM4H5; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -3665,6 +4083,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.owncloud.ios-app.ownCloud-Intents"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development com.owncloud.ios-app.ownCloud-Intents"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -3678,7 +4097,7 @@ CODE_SIGN_ENTITLEMENTS = "ownCloud Intents/ownCloud Intents.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = 4AP2STM4H5; INFOPLIST_FILE = "ownCloud Intents/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -3686,6 +4105,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.owncloud.ios-app.ownCloud-Intents"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.owncloud.ios-app.ownCloud-Intents"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -3781,6 +4201,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = NO; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -3818,6 +4239,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = NO; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index ff3da31ca..b1ead6067 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -154,6 +154,11 @@ value = "1" isEnabled = "YES"> + + + isEnabled = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 3dea80ea0..682e0050d 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -18,6 +18,8 @@ import UIKit import ownCloudSDK +import ownCloudApp +import ownCloudAppShared @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -31,6 +33,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Set up logging (incl. stderr redirection) and log launch time, app version, build number and commit Log.log("ownCloud \(VendorServices.shared.appVersion) (\(VendorServices.shared.appBuildNumber)) #\(LastGitCommit() ?? "unknown") finished launching with log settings: \(Log.logOptionStatus)") + // Set up license management + OCLicenseManager.shared.setupLicenseManagement() + // Set up app window = ThemeWindow(frame: UIScreen.main.bounds) @@ -80,8 +85,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(LinksAction.actionExtension) OCExtensionManager.shared.addExtension(FavoriteAction.actionExtension) OCExtensionManager.shared.addExtension(UnfavoriteAction.actionExtension) - // OCExtensionManager.shared.addExtension(ScanAction.actionExtension) - + OCExtensionManager.shared.addExtension(ScanAction.actionExtension) if #available(iOS 13.0, *), UIDevice.current.isIpad() { OCExtensionManager.shared.addExtension(DiscardSceneAction.actionExtension) OCExtensionManager.shared.addExtension(OpenSceneAction.actionExtension) diff --git a/ownCloud/Client/Actions/Action.swift b/ownCloud/Client/Actions/Action.swift index 3d09fbc3d..370be4c91 100644 --- a/ownCloud/Client/Actions/Action.swift +++ b/ownCloud/Client/Actions/Action.swift @@ -18,6 +18,7 @@ import UIKit import ownCloudSDK +import ownCloudApp enum ActionCategory { case normal @@ -298,13 +299,61 @@ class Action : NSObject { } } + // MARK: - Licensing + class var licenseRequirements : LicenseRequirements? { return nil } + + var isLicensed : Bool { + guard let core = self.core else { + return false + } + + if let licenseRequirements = type(of:self).licenseRequirements, !licenseRequirements.isUnlocked(for: core) { + return false + } + + return true + } + + func proceedWithLicensing(from viewController: UIViewController) -> Bool { + if !isLicensed { + if let core = core, let requirements = type(of:self).licenseRequirements { + OnMainThread { + OCLicenseManager.appStoreProvider?.refreshProductsIfNeeded(completionHandler: { (error) in + OnMainThread { + if error != nil { + let alertController = ThemedAlertController(with: "Error loading product info from App Store".localized, message: error!.localizedDescription) + + viewController.present(alertController, animated: true) + } else { + let offersViewController = LicenseOffersViewController(withFeature: requirements.feature, in: core.licenseEnvironment) + + viewController.present(asCard: MoreViewController(header: offersViewController.cardHeaderView!, viewController: offersViewController), animated: true) + } + } + }) + } + } + + return false + } + + return true + } + // MARK: - Action UI elements private static let staticRowImageWidth : CGFloat = 32 + private let proLabel = "ᴾᴿᴼ" // "🅿🆁🅾" func provideStaticRow() -> StaticTableViewRow? { + var name = actionExtension.name + + if !isLicensed { + name += " " + proLabel + } + return StaticTableViewRow(buttonWithAction: { (_ row, _ sender) in self.perform() - }, title: actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .plain, image: self.icon, imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: actionExtension.identifier.rawValue) + }, title: name, style: actionExtension.category == .destructive ? .destructive : .plain, image: self.icon, imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: actionExtension.identifier.rawValue) } func provideContextualAction() -> UIContextualAction? { @@ -315,7 +364,13 @@ class Action : NSObject { } func provideAlertAction() -> UIAlertAction? { - let alertAction = UIAlertAction(title: self.actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .default, handler: { (_ alertAction) in + var name = actionExtension.name + + if !isLicensed { + name += " " + proLabel + } + + let alertAction = UIAlertAction(title: name, style: actionExtension.category == .destructive ? .destructive : .default, handler: { (_ alertAction) in self.perform() }) diff --git a/ownCloud/Client/Actions/MoreViewController.swift b/ownCloud/Client/Actions/MoreViewController.swift index cf59e421e..f11a71635 100644 --- a/ownCloud/Client/Actions/MoreViewController.swift +++ b/ownCloud/Client/Actions/MoreViewController.swift @@ -75,7 +75,10 @@ class MoreViewController: UIViewController, CardPresentationSizing { headerView.topAnchor.constraint(equalTo: view.topAnchor) ]) + self.addChild(viewController) view.addSubview(viewController.view) + viewController.didMove(toParent: self) + viewController.view.translatesAutoresizingMaskIntoConstraints = false let bottomConstraint = viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) diff --git a/ownCloud/Client/Actions/Scanner/ScanAction.swift b/ownCloud/Client/Actions/Scanner/ScanAction.swift index a60334138..0938e4bf7 100644 --- a/ownCloud/Client/Actions/Scanner/ScanAction.swift +++ b/ownCloud/Client/Actions/Scanner/ScanAction.swift @@ -17,6 +17,8 @@ */ import ownCloudSDK +import ownCloudApp +import ownCloudAppShared import VisionKit class ScanAction: Action, VNDocumentCameraViewControllerDelegate { @@ -26,6 +28,7 @@ class ScanAction: Action, VNDocumentCameraViewControllerDelegate { override class var locations : [OCExtensionLocationIdentifier]? { return [ .folderAction, .keyboardShortcut ] } override class var keyCommand : String? { return "S" } override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } + override class var licenseRequirements: LicenseRequirements? { return LicenseRequirements(feature: .documentScanner) } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { @@ -50,6 +53,10 @@ class ScanAction: Action, VNDocumentCameraViewControllerDelegate { return } + guard self.proceedWithLicensing(from: viewController) else { + return + } + guard context.items.count > 0 else { completed(with: NSError(ocError: .itemNotFound)) return diff --git a/ownCloud/Client/Viewer/DisplayHostViewController.swift b/ownCloud/Client/Viewer/DisplayHostViewController.swift index 12dce8cf3..c968410ed 100644 --- a/ownCloud/Client/Viewer/DisplayHostViewController.swift +++ b/ownCloud/Client/Viewer/DisplayHostViewController.swift @@ -107,9 +107,9 @@ class DisplayHostViewController: UIPageViewController { } NotificationCenter.default.addObserver(self, selector: #selector(handleMediaPlaybackFinished(notification:)), name: MediaDisplayViewController.MediaPlaybackFinishedNotification, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(handlePlayNextMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackNextTrackNotification, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(handlePlayPreviousMedia(notification:)), name: MediaDisplayViewController.MediaPlaybackPreviousTrackNotification, object: nil) } @@ -331,7 +331,7 @@ extension DisplayHostViewController: Themeable { } extension DisplayHostViewController { - + @objc private func handleMediaPlaybackFinished(notification:Notification) { if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { if let vc = vendNewViewController(from: mediaController, .after) { @@ -339,7 +339,7 @@ extension DisplayHostViewController { } } } - + @objc private func handlePlayNextMedia(notification:Notification) { if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { if let vc = vendNewViewController(from: mediaController, .after) { @@ -347,7 +347,7 @@ extension DisplayHostViewController { } } } - + @objc private func handlePlayPreviousMedia(notification:Notification) { if let mediaController = self.viewControllers?.first as? MediaDisplayViewController { if let vc = vendNewViewController(from: mediaController, .before) { diff --git a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift index 50bb57b76..fff47a369 100644 --- a/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift +++ b/ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift @@ -42,7 +42,7 @@ class MediaDisplayViewController : DisplayViewController { deinit { playerStatusObservation?.invalidate() playerItemStatusObservation?.invalidate() - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) @@ -253,7 +253,7 @@ class MediaDisplayViewController : DisplayViewController { } return .commandFailed } - + // TODO: Skip controls are useful for podcasts but not so much for music. // Disable them for now but keep the implementation of command handlers commandCenter.skipForwardCommand.isEnabled = false @@ -267,15 +267,15 @@ class MediaDisplayViewController : DisplayViewController { if itemIndex > 0 { enablePreviousTrackCommand = true } - + if let displayHostController = self.parent as? DisplayHostViewController, let items = displayHostController.items { enableNextTrackCommand = itemIndex < (items.count - 1) } } - - commandCenter.nextTrackCommand.isEnabled = enableNextTrackCommand + + commandCenter.nextTrackCommand.isEnabled = enableNextTrackCommand commandCenter.previousTrackCommand.isEnabled = enablePreviousTrackCommand - + // Add handler for seek forward command commandCenter.nextTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in if let player = self?.player { @@ -287,7 +287,7 @@ class MediaDisplayViewController : DisplayViewController { } return .commandFailed } - + // Add handler for seek backward command commandCenter.previousTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in if let player = self?.player { @@ -304,10 +304,10 @@ class MediaDisplayViewController : DisplayViewController { private func updateNowPlayingTimeline() { MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.playerItem?.currentTime().seconds - + MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = self.player?.rate } - + private func updateNowPlayingInfoCenter() { guard let player = self.player else { return } guard let playerItem = self.playerItem else { return } @@ -328,7 +328,6 @@ class MediaDisplayViewController : DisplayViewController { } MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - updateNowPlayingTimeline() } } diff --git a/ownCloud/Key Commands/KeyCommands.swift b/ownCloud/Key Commands/KeyCommands.swift index 2b07f1380..953d72b6a 100644 --- a/ownCloud/Key Commands/KeyCommands.swift +++ b/ownCloud/Key Commands/KeyCommands.swift @@ -782,7 +782,7 @@ extension QueryFileListTableViewController { } @objc func changeSortMethod(_ command : UIKeyCommand) { - for (_, method) in SortMethod.all.enumerated() { + for method in SortMethod.all { let sortTitle = String(format: "Sort by %@".localized, method.localizedName()) if command.discoverabilityTitle == sortTitle { self.sortBar?.sortMethod = method diff --git a/ownCloud/Licensing/LicenseRequirements.swift b/ownCloud/Licensing/LicenseRequirements.swift new file mode 100644 index 000000000..a47d40c69 --- /dev/null +++ b/ownCloud/Licensing/LicenseRequirements.swift @@ -0,0 +1,28 @@ +// +// LicenseRequirements.swift +// ownCloud +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +struct LicenseRequirements { + var feature : OCLicenseFeatureIdentifier + + func isUnlocked(for core: OCCore) -> Bool { + return OCLicenseManager.shared.authorizationStatus(forFeature: self.feature, in: core.licenseEnvironment) == .granted + } +} diff --git a/ownCloud/Licensing/Offers/LicenseOfferButton.swift b/ownCloud/Licensing/Offers/LicenseOfferButton.swift new file mode 100644 index 000000000..2b887a4ae --- /dev/null +++ b/ownCloud/Licensing/Offers/LicenseOfferButton.swift @@ -0,0 +1,62 @@ +// +// LicenseOfferButton.swift +// ownCloud +// +// Created by Felix Schwarz on 11.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +class LicenseOfferButton: ThemeButton { + var originalTitle : String? + + init(purchaseButtonWithTitle title: String, target: Any? = nil, action: Selector? = nil) { + super.init(frame: .zero) + + self.translatesAutoresizingMaskIntoConstraints = false + self.setContentCompressionResistancePriority(.required, for: .horizontal) + self.setContentCompressionResistancePriority(.required, for: .vertical) + self.buttonFont = UIFont.systemFont(ofSize: UIFont.labelFontSize) + self.buttonVerticalPadding = -5 + self.buttonHorizontalPadding = 23 + self.buttonCornerRadius = -1 + + originalTitle = title + self.setTitle(title, for: .normal) + + if let action = action { + self.addTarget(target, action: action, for: .primaryActionTriggered) + } + } + + init(subscribeButtonWithTitle title: String, target: Any? = nil, action: Selector? = nil) { + super.init(frame: .zero) + + self.translatesAutoresizingMaskIntoConstraints = false + self.buttonFont = UIFont.systemFont(ofSize: UIFont.labelFontSize) + + self.buttonVerticalPadding = 15 + self.buttonCornerRadius = 10 + + self.setTitle(title, for: .normal) + + if let action = action { + self.addTarget(target, action: action, for: .primaryActionTriggered) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/ownCloud/Licensing/Offers/LicenseOfferView.swift b/ownCloud/Licensing/Offers/LicenseOfferView.swift new file mode 100644 index 000000000..e5c583e3e --- /dev/null +++ b/ownCloud/Licensing/Offers/LicenseOfferView.swift @@ -0,0 +1,282 @@ +// +// LicenseOfferView.swift +// ownCloud +// +// Created by Felix Schwarz on 11.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +class LicenseOfferView: UIView, Themeable { + var offer: OCLicenseOffer + var feature: OCLicenseFeature? + var environment: OCLicenseEnvironment + weak var baseViewController: UIViewController? + + private var stateObservation : NSKeyValueObservation? + + init(with offer: OCLicenseOffer, focusedOn feature: OCLicenseFeature? = nil, in environment: OCLicenseEnvironment, baseViewController: UIViewController?) { + self.offer = offer + self.feature = feature + self.environment = environment + self.baseViewController = baseViewController + + super.init(frame: .zero) + self.translatesAutoresizingMaskIntoConstraints = false + + buildView() + + stateObservation = self.offer.observe(\OCLicenseOffer.state, options: .initial) { [weak self] (_, _) in + self?.updateOfferFromState() + } + + Theme.shared.register(client: self, applyImmediately: true) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + Theme.shared.unregister(client: self) + } + + var localizedTitle : String { + return offer.localizedTitle ?? (offer.product?.localizedName ?? offer.productIdentifier.rawValue) + } + + var localizedDescription : String { + return offer.localizedDescription ?? (offer.product?.localizedDescription ?? offer.productIdentifier.rawValue) + } + + var titleLabel : UILabel? + var descriptionLabel : UILabel? + + var pricingDivider : UIView? + var pricingLabel : UILabel? + + var purchaseButton : LicenseOfferButton? + var purchaseBusyView : ProgressView? + + private let titleLabelSize : CGFloat = 20 + private let descriptionLabelSize : CGFloat = 17 + private let tryLabelSize : CGFloat = 17 + private let priceLabelSize : CGFloat = 15 + + func buildView() { + titleLabel = UILabel() + descriptionLabel = UILabel() + + guard let titleLabel = titleLabel else { return } + guard let descriptionLabel = descriptionLabel else { return } + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + + titleLabel.font = UIFont.systemFont(ofSize: self.titleLabelSize, weight: .semibold) + descriptionLabel.font = UIFont.systemFont(ofSize: self.descriptionLabelSize) + + descriptionLabel.numberOfLines = 0 + + titleLabel.text = self.localizedTitle + descriptionLabel.text = self.localizedDescription + + self.addSubview(titleLabel) + self.addSubview(descriptionLabel) + + titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + descriptionLabel.setContentCompressionResistancePriority(.required, for: .vertical) + self.setContentCompressionResistancePriority(.required, for: .vertical) + + var constraints = [ + titleLabel.leftAnchor.constraint(equalTo: self.leftAnchor), + titleLabel.topAnchor.constraint(equalTo: self.topAnchor), + + descriptionLabel.leftAnchor.constraint(equalTo: self.leftAnchor), + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5) + ] + + if offer.type == .purchase { + purchaseButton = LicenseOfferButton(purchaseButtonWithTitle: offer.localizedPriceTag, target: self, action: #selector(takeOffer)) + guard let purchaseButton = purchaseButton else { return } + + self.addSubview(purchaseButton) + + constraints.append(contentsOf: [ + purchaseButton.rightAnchor.constraint(equalTo: self.rightAnchor), + purchaseButton.topAnchor.constraint(equalTo: self.topAnchor), + + titleLabel.rightAnchor.constraint(lessThanOrEqualTo: purchaseButton.leftAnchor, constant: -10), + descriptionLabel.rightAnchor.constraint(lessThanOrEqualTo: purchaseButton.leftAnchor, constant: -10) + ]) + } else { + constraints.append(contentsOf: [ + titleLabel.rightAnchor.constraint(equalTo: self.rightAnchor), + descriptionLabel.rightAnchor.constraint(equalTo: self.rightAnchor) + ]) + } + + if offer.type == .subscription { + pricingLabel = UILabel() + pricingLabel?.translatesAutoresizingMaskIntoConstraints = false + guard let pricingLabel = pricingLabel else { return } + + purchaseButton = LicenseOfferButton(subscribeButtonWithTitle: "Subscribe Now".localized, target: self, action: #selector(takeOffer)) + guard let purchaseButton = purchaseButton else { return } + + var pricingLabelText : String = "" + var trialTextLength : Int = 0 + + if let trialDuration = offer.trialDuration, offer.state(in: environment) != .expired { + pricingLabelText = NSString(format: "Try %@ for free.".localized as NSString, trialDuration.localizedDescription) as String + pricingLabelText = "\(pricingLabelText)\n" + trialTextLength = pricingLabelText.count + + pricingLabelText = pricingLabelText.appendingFormat("Then %@ / %@.".localized, offer.localizedPriceTag, offer.subscriptionTermDuration.localizedDescription) + } else { + // No trial available (either in general, or because user already has subscribed once) + pricingLabelText = pricingLabelText.appendingFormat("%@ / %@ – starting immediately".localized, offer.localizedPriceTag, offer.subscriptionTermDuration.localizedDescription) + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.5 + + let formattedPricingLabelText = NSMutableAttributedString(string: pricingLabelText, attributes: [ + .font : UIFont.systemFont(ofSize: priceLabelSize), + .paragraphStyle : paragraphStyle + ]) + + if trialTextLength > 0 { + formattedPricingLabelText.addAttribute(.font, value: UIFont.systemFont(ofSize: tryLabelSize, weight: .bold), range: NSRange(location: 0, length: trialTextLength)) + } + + pricingLabel.attributedText = formattedPricingLabelText + pricingLabel.numberOfLines = 0 + pricingLabel.setContentCompressionResistancePriority(.required, for: .vertical) + + titleLabel.textAlignment = .center + descriptionLabel.textAlignment = .center + pricingLabel.textAlignment = .center + + pricingDivider = UIView() + pricingDivider?.translatesAutoresizingMaskIntoConstraints = false + guard let pricingDivider = pricingDivider else { return } + + self.addSubview(pricingLabel) + self.addSubview(purchaseButton) + self.addSubview(pricingDivider) + + constraints.append(contentsOf: [ + pricingDivider.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 10), + pricingDivider.heightAnchor.constraint(equalToConstant: 1), + pricingDivider.leftAnchor.constraint(equalTo: self.leftAnchor), + pricingDivider.rightAnchor.constraint(equalTo: self.rightAnchor), + + pricingLabel.topAnchor.constraint(equalTo: pricingDivider.bottomAnchor, constant: 10), + pricingLabel.leftAnchor.constraint(equalTo: self.leftAnchor), + pricingLabel.rightAnchor.constraint(equalTo: self.rightAnchor), + + purchaseButton.topAnchor.constraint(equalTo: pricingLabel.bottomAnchor, constant: 20), + purchaseButton.leftAnchor.constraint(equalTo: self.leftAnchor), + purchaseButton.rightAnchor.constraint(equalTo: self.rightAnchor), + purchaseButton.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + } else { + constraints.append(contentsOf: [ + descriptionLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + } + + if let purchaseButton = purchaseButton { + let progress = Progress.indeterminate() + progress?.isCancellable = false + + purchaseBusyView = ProgressView() + purchaseBusyView?.translatesAutoresizingMaskIntoConstraints = false + purchaseBusyView?.progress = progress + + guard let purchaseBusyView = purchaseBusyView else { return } + self.addSubview(purchaseBusyView) + + constraints.append(contentsOf: [ + purchaseBusyView.centerXAnchor.constraint(equalTo: purchaseButton.centerXAnchor), + purchaseBusyView.centerYAnchor.constraint(equalTo: purchaseButton.centerYAnchor) + ]) + } + + NSLayoutConstraint.activate(constraints) + } + + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + titleLabel?.applyThemeCollection(collection) + descriptionLabel?.applyThemeCollection(collection) + pricingLabel?.applyThemeCollection(collection) + purchaseButton?.applyThemeCollection(collection, itemStyle: .purchase) + + pricingDivider?.backgroundColor = collection.tableSeparatorColor ?? .gray + } + + @objc func takeOffer() { + offer.commit(options: [ + .baseViewController : self.baseViewController as Any + ], errorHandler: { [weak self] (error) in + guard let error = error else { return } + + OnMainThread { + guard let self = self else { return } + + let alertController = UIAlertController(title: "Purchase failed".localized, message: error.localizedDescription, preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + + self.baseViewController?.present(alertController, animated: true, completion: nil) + } + }) + } + + func updateOfferFromState() { + var buttonTitle = purchaseButton?.originalTitle + var buttonEnabled = true + + purchaseBusyView?.isHidden = true + purchaseButton?.isHidden = false + + switch offer.state(in: environment) { + case .uncommitted, .expired: + buttonEnabled = true + + case .unavailable: + buttonEnabled = false + + case .redundant: + buttonEnabled = false + buttonTitle = "Unlocked".localized + + case .inProgress: + purchaseButton?.isHidden = true + purchaseBusyView?.isHidden = false + + case .committed: + buttonEnabled = false + buttonTitle = "Unlocked".localized + } + + purchaseButton?.isEnabled = buttonEnabled + + if let buttonTitle = buttonTitle { + purchaseButton?.setTitle(buttonTitle, for: .normal) + } + } +} diff --git a/ownCloud/Licensing/Offers/LicenseOffersViewController.swift b/ownCloud/Licensing/Offers/LicenseOffersViewController.swift new file mode 100644 index 000000000..9500526ca --- /dev/null +++ b/ownCloud/Licensing/Offers/LicenseOffersViewController.swift @@ -0,0 +1,148 @@ +// +// LicenseOffersViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 30.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +class LicenseOffersViewController: StaticTableViewController { + var featureIdentifier: OCLicenseFeatureIdentifier + var environment: OCLicenseEnvironment + + init(withFeature featureIdentifier: OCLicenseFeatureIdentifier, in environment: OCLicenseEnvironment) { + self.featureIdentifier = featureIdentifier + self.environment = environment + + super.init(style: .grouped) + + setupHeaderView() + composeSections() + + OCLicenseManager.shared.observeProducts(nil, features: [featureIdentifier], in: environment, withOwner: self, updateHandler: { (observer, _, authStatus) in + if authStatus == .granted { + OnMainThread(after: 1.5) { + (observer.owner as? LicenseOffersViewController)?.dismissAnimated() + } + } + }) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var cardHeaderView : UIView? + + func setupHeaderView() { + let headerView = UIView() + let titleView = UILabel() + let fontSize : CGFloat = 22 + + let headerText = NSMutableAttributedString(string: "Pro Features".localized, attributes: [ + .font : UIFont.systemFont(ofSize: fontSize, weight: .light) + ]) + + headerText.addAttribute(.font, value: UIFont.systemFont(ofSize: fontSize, weight: .semibold), range: (headerText.string as NSString).range(of: "Pro")) + + titleView.translatesAutoresizingMaskIntoConstraints = false + titleView.attributedText = headerText + + titleView.applyThemeCollection(Theme.shared.activeCollection) + + headerView.addSubview(titleView) + + NSLayoutConstraint.activate([ + titleView.leftAnchor.constraint(equalTo: headerView.leftAnchor, constant: 15), + titleView.rightAnchor.constraint(equalTo: headerView.rightAnchor, constant: -15), + titleView.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 10), + titleView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -10) + ]) + + self.cardHeaderView = headerView + } + + func composeSections() { + let iapSection = StaticTableViewSection(headerTitle: "Purchase") + let subSection = StaticTableViewSection(headerTitle: "Subscribe") + + if let feature = OCLicenseManager.shared.feature(withIdentifier: featureIdentifier) { + if let offers = OCLicenseManager.shared.offers(for: feature) { + for offer in offers { + + let offerRow = StaticTableViewRow(customView: LicenseOfferView(with: offer, focusedOn: feature, in: environment, baseViewController: self), inset: UIEdgeInsets(top: 15, left: 18, bottom: 15, right: 18)) + + switch offer.type { + case .subscription: + subSection.add(row: offerRow) + + case .purchase: + iapSection.add(row: offerRow) + + default: break + } + } + } + } + + // (Re)build sections + var sections : [StaticTableViewSection] = [] + + if iapSection.rows.count > 0 { + sections.append(iapSection) + } + + if subSection.rows.count > 0 { + sections.append(subSection) + } + + let restoreSection = StaticTableViewSection() + + restoreSection.add(row: StaticTableViewRow(rowWithAction: { (_, _) in + OCLicenseManager.shared.restorePurchases(on: self, with: { (_) in + self.composeSections() + }) + }, title: "Restore purchases".localized, alignment: .center)) + + sections.append(restoreSection) + + // Set sections + self.sections = sections + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + if sections[section].headerTitle != nil { + return 40 + } + + return 20 + } + + override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + if sections[section].footerTitle != nil { + return super.tableView(tableView, heightForFooterInSection: section) + } + + return .leastNormalMagnitude + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.tableView.sectionFooterHeight = .leastNormalMagnitude + self.tableView.contentInsetAdjustmentBehavior = .never + } +} diff --git a/ownCloud/Licensing/Product List/LicenseInAppProductListViewController.swift b/ownCloud/Licensing/Product List/LicenseInAppProductListViewController.swift new file mode 100644 index 000000000..4d8c36d44 --- /dev/null +++ b/ownCloud/Licensing/Product List/LicenseInAppProductListViewController.swift @@ -0,0 +1,94 @@ +// +// LicenseInAppProductListViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 14.01.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +class LicenseInAppProductListViewController: StaticTableViewController { + init() { + super.init(style: .grouped) + + self.navigationItem.title = "Pro Features".localized + + self.toolbarItems = [ + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + UIBarButtonItem(title: "Restore purchases".localized, style: .plain, target: self, action: #selector(restorePurchases)), + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + ] + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.toolbar.isTranslucent = false + self.navigationController?.isToolbarHidden = false + + provideContent() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + self.navigationController?.isToolbarHidden = true + } + + func provideContent() { + OCLicenseManager.appStoreProvider?.refreshProductsIfNeeded(completionHandler: { [weak self] (error) in + OnMainThread { + if error != nil { + let alertController = ThemedAlertController(with: "Error loading product info from App Store".localized, message: error!.localizedDescription, action: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }) + + self?.present(alertController, animated: true) + } else { + self?.generateContent() + } + } + }) + } + + func generateContent() { + if self.sections.count == 0 { + let section = StaticTableViewSection(headerTitle: "Pro Features".localized) + let environment = OCLicenseEnvironment() + + if let features = OCLicenseManager.shared.features(withOffers: true) { + for feature in features { + section.add(row: StaticTableViewRow(customView: LicenseInAppPurchaseFeatureView(with: feature, in: environment, baseViewController: self), inset: UIEdgeInsets(top: 15, left: 18, bottom: 15, right: 18))) + } + } + + self.addSection(section) + } + } + + @objc func restorePurchases() { + OCLicenseManager.shared.restorePurchases(on: self) { (error) in + if error == nil { + self.removeSections(self.sections) + self.generateContent() + } + } + } + +} diff --git a/ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift b/ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift new file mode 100644 index 000000000..963e6acf7 --- /dev/null +++ b/ownCloud/Licensing/Product List/LicenseInAppPurchaseFeatureView.swift @@ -0,0 +1,135 @@ +// +// LicenseInAppPurchaseFeatureView.swift +// ownCloud +// +// Created by Felix Schwarz on 15.01.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +class LicenseInAppPurchaseFeatureView: UIView, Themeable { + var feature: OCLicenseFeature + var environment: OCLicenseEnvironment + weak var baseViewController: UIViewController? + + private var stateObservation : NSKeyValueObservation? + + init(with feature: OCLicenseFeature, in environment: OCLicenseEnvironment, baseViewController: UIViewController?) { + self.feature = feature + self.environment = environment + self.baseViewController = baseViewController + + super.init(frame: .zero) + self.translatesAutoresizingMaskIntoConstraints = false + + buildView() + + OCLicenseManager.shared.observeProducts(nil, features: [feature.identifier], in: environment, withOwner: self) { [weak self] (_, _, status) in + guard let button = self?.purchaseButton else { return } + + switch status { + case .unknown, .denied, .expired: + button.setTitle("Unlock".localized, for: .normal) + button.isEnabled = true + + case .granted: + button.setTitle("Unlocked".localized, for: .normal) + button.isEnabled = false + } + + button.invalidateIntrinsicContentSize() + } + + Theme.shared.register(client: self, applyImmediately: true) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + Theme.shared.unregister(client: self) + } + + var titleLabel : UILabel? + var descriptionLabel : UILabel? + + var purchaseButton : LicenseOfferButton? + + private let titleLabelSize : CGFloat = 20 + private let descriptionLabelSize : CGFloat = 17 + + func buildView() { + titleLabel = UILabel() + descriptionLabel = UILabel() + + guard let titleLabel = titleLabel else { return } + guard let descriptionLabel = descriptionLabel else { return } + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + + titleLabel.font = UIFont.systemFont(ofSize: self.titleLabelSize, weight: .semibold) + descriptionLabel.font = UIFont.systemFont(ofSize: self.descriptionLabelSize) + + descriptionLabel.numberOfLines = 0 + + titleLabel.text = feature.localizedName ?? feature.identifier.rawValue + descriptionLabel.text = feature.localizedDescription ?? "" + + self.addSubview(titleLabel) + self.addSubview(descriptionLabel) + + titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + descriptionLabel.setContentCompressionResistancePriority(.required, for: .vertical) + self.setContentCompressionResistancePriority(.required, for: .vertical) + + var constraints = [ + titleLabel.leftAnchor.constraint(equalTo: self.leftAnchor), + titleLabel.topAnchor.constraint(equalTo: self.topAnchor), + + descriptionLabel.leftAnchor.constraint(equalTo: self.leftAnchor), + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5) + ] + + purchaseButton = LicenseOfferButton(purchaseButtonWithTitle: "", target: self, action: #selector(takeOffer)) + guard let purchaseButton = purchaseButton else { return } + + self.addSubview(purchaseButton) + + constraints.append(contentsOf: [ + purchaseButton.rightAnchor.constraint(equalTo: self.rightAnchor), + purchaseButton.topAnchor.constraint(equalTo: self.topAnchor), + + titleLabel.rightAnchor.constraint(lessThanOrEqualTo: purchaseButton.leftAnchor, constant: -10), + descriptionLabel.rightAnchor.constraint(lessThanOrEqualTo: purchaseButton.leftAnchor, constant: -10), + descriptionLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) + + NSLayoutConstraint.activate(constraints) + } + + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + titleLabel?.applyThemeCollection(collection) + descriptionLabel?.applyThemeCollection(collection) + purchaseButton?.applyThemeCollection(collection, itemStyle: .purchase) + } + + @objc func takeOffer() { + let offersViewController = LicenseOffersViewController(withFeature: feature.identifier, in: environment) + + baseViewController?.present(asCard: MoreViewController(header: offersViewController.cardHeaderView!, viewController: offersViewController), animated: true) + } +} diff --git a/ownCloud/Licensing/Tools/OCLicenseManager+AppStore.swift b/ownCloud/Licensing/Tools/OCLicenseManager+AppStore.swift new file mode 100644 index 000000000..c4ce0850a --- /dev/null +++ b/ownCloud/Licensing/Tools/OCLicenseManager+AppStore.swift @@ -0,0 +1,55 @@ +// +// OCLicenseManager+AppStore.swift +// ownCloud +// +// Created by Felix Schwarz on 12.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +extension OCLicenseManager { + @objc func restorePurchases(on viewController: UIViewController, with completionHandler: OCLicenseAppStoreRestorePurchasesCompletionHandler? = nil) { + if let appStoreProvider = OCLicenseManager.appStoreProvider { + let hud : ProgressHUDViewController? = ProgressHUDViewController(on: nil) + + hud?.present(on: viewController, label: "Restoring purchases…".localized) + + appStoreProvider.restorePurchases(completionHandler: { (error) in + let completion = { + OnMainThread { + if let error = error { + let alert = ThemedAlertController(title: "Error restoring purchases".localized, message: error.localizedDescription, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + + viewController.present(alert, animated: true, completion: nil) + } + + completionHandler?(error) + } + } + + OnMainThread { + if hud != nil { + hud?.dismiss(completion: completion) + } else { + completion() + } + } + }) + } + } + +} diff --git a/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift b/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift new file mode 100644 index 000000000..f11621d8c --- /dev/null +++ b/ownCloud/Licensing/Transactions/LicenseTransactionsViewController.swift @@ -0,0 +1,112 @@ +// +// LicenseTransactionsViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +class LicenseTransactionsViewController: StaticTableViewController { + init() { + super.init(style: .grouped) + + self.navigationItem.title = "Purchases".localized + + self.toolbarItems = [ + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + UIBarButtonItem(title: "Restore purchases".localized, style: .plain, target: self, action: #selector(restorePurchases)), + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + ] + + fetchTransactions() + } + + func fetchTransactions() { + OCLicenseManager.shared.retrieveAllTransactions(completionHandler: { (error, transactionsByProvider) in + if let error = error { + let alert = UIAlertController(title: "Error fetching transactions".localized, message: error.localizedDescription, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + + self.present(alert, animated: true, completion: nil) + } + + if let transactionsByProvider = transactionsByProvider { + self.generateContent(from: transactionsByProvider) + } + }) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.toolbar.isTranslucent = false + self.navigationController?.isToolbarHidden = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + self.navigationController?.isToolbarHidden = true + } + + func generateContent(from transactionsByProvider : [[OCLicenseTransaction]]) { + for transactions in transactionsByProvider { + var firstTransaction = true + + let sortedTransactions = transactions.sorted { (t1, t2) in + return (t1.date?.timeIntervalSinceReferenceDate ?? Double.greatestFiniteMagnitude) > (t2.date?.timeIntervalSinceReferenceDate ?? Double.greatestFiniteMagnitude) + } + + for transaction in sortedTransactions { + if let tableRows = transaction.displayTableRows, tableRows.count > 0 { + let section = StaticTableViewSection(headerTitle: firstTransaction ? transaction.provider?.localizedName : nil) + + for tableRow in tableRows { + for (label, value) in tableRow { + section.add(row: StaticTableViewRow(valueRowWithAction: nil, title: label, value: value)) + } + } + + if let links = transaction.links { + for (title, url) in links { + section.add(row: StaticTableViewRow(rowWithAction: { (_, _) in + UIApplication.shared.open(url, options: [:], completionHandler: nil) + }, title: title, alignment: .center)) + } + } + + firstTransaction = false + + self.addSection(section) + } + } + } + } + + @objc func restorePurchases() { + OCLicenseManager.shared.restorePurchases(on: self) { (error) in + if error == nil { + self.removeSections(self.sections) + self.fetchTransactions() + } + } + } +} diff --git a/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift b/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift index f83b87da1..6a232338e 100644 --- a/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift +++ b/ownCloud/PhotoKit Extensions/MediaUploadActivity.swift @@ -28,7 +28,7 @@ class MediaUploadActivity : OCActivity { } init(identifier: String, assetCount:Int) { - super.init(identifier: identifier) + super.init(identifier: OCActivityIdentifier(rawValue: identifier)) self.isCancellable = true self.localizedDescription = "Media import".localized diff --git a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift index c3efc2dcc..631fea286 100644 --- a/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift +++ b/ownCloud/PhotoKit Extensions/MediaUploadQueue.swift @@ -22,7 +22,6 @@ import Photos import MobileCoreServices class MediaUploadQueue : OCActivitySource { - private var uploadActivity: MediaUploadActivity? static var shared = MediaUploadQueue() @@ -33,11 +32,11 @@ class MediaUploadQueue : OCActivitySource { return self.uploadActivity! } - var activityIdentifier: String { + var activityIdentifier: OCActivityIdentifier { if let activity = self.uploadActivity { return activity.identifier } else { - return "" + return OCActivityIdentifier(rawValue: "") } } diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 41dc7cd13..bcd5a1ea7 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -516,3 +516,50 @@ "Previous" = "Previous"; "Favorite" = "Favorite"; "Cut" = "Cut"; + +/* Licensing */ +"Unlocked" = "Unlocked"; +"Unlock" = "Unlock"; + +"Subscribe Now" = "Subscribe Now"; +"Free" = "Free"; + +"day" = "day"; +"%lu days" = "%lu days"; + +"week" = "week"; +"%lu weeks" = "%lu weeks"; + +"month" = "month"; +"%lu months" = "%lu months"; + +"year" = "year"; +"%lu years" = "%lu years"; + +/* Example usage: "Try [14 days] for free." */ +"Try %@ for free." = "Try %@ for free."; + +/* Example usage: "Then [1,99 €] / [year]." */ +"Then %@ / %@." = "Then %@ / %@."; + +/* Example usage: "[1,99 €] / [year] - starting immediately." */ +"%@ / %@ – starting immediately" = "%@ / %@ – starting immediately"; + +/* Licensing: Pro Features */ +"Pro Features" = "Pro Features"; + +/* Licensing: App Store */ +"Restore purchases" = "Restore purchases"; +"Restoring purchases…" = "Restoring purchases…"; +"Error restoring purchases" = "Error restoring purchases"; +"Error loading product info from App Store"= "Error loading product info from App Store"; +"Purchase failed" = "Purchase failed"; + +/* Licensing: Enterprise */ +"Enterprise" = "Enterprise"; + +/* Licensing: Settings */ +"In-App Purchases" = "In-App Purchases"; + +"Purchases" = "Purchases"; +"Error fetching transactions" = "Error fetching transactions"; diff --git a/ownCloud/SDK Extensions/OCBookmark+Extension.swift b/ownCloud/SDK Extensions/OCBookmark+Extension.swift index aa1458352..814660350 100644 --- a/ownCloud/SDK Extensions/OCBookmark+Extension.swift +++ b/ownCloud/SDK Extensions/OCBookmark+Extension.swift @@ -20,7 +20,7 @@ import UIKit import ownCloudSDK extension OCBookmark { - static let OCBookmarkDisplayName : NSString = "OCBookmarkDisplayName" + static let OCBookmarkDisplayName : OCBookmarkUserInfoKey = OCBookmarkUserInfoKey(rawValue: "OCBookmarkDisplayName") var userName : String? { if let authenticationData = self.authenticationData, diff --git a/ownCloud/Settings/MoreSettingsSection.swift b/ownCloud/Settings/MoreSettingsSection.swift index c5c08f7f9..3180e7b4f 100644 --- a/ownCloud/Settings/MoreSettingsSection.swift +++ b/ownCloud/Settings/MoreSettingsSection.swift @@ -22,6 +22,7 @@ import WebKit import MessageUI import SafariServices import ownCloudSDK +import ownCloudApp class MoreSettingsSection: SettingsSection { // MARK: - More Settings Cells @@ -65,7 +66,7 @@ class MoreSettingsSection: SettingsSection { if let viewController = self?.viewController { VendorServices.shared.sendFeedback(from: viewController) } - }, title: "Send feedback".localized, accessoryType: .disclosureIndicator, identifier: "send-feedback") + }, title: "Send feedback".localized, accessoryType: .disclosureIndicator, identifier: "send-feedback") recommendRow = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in if let viewController = self?.viewController { diff --git a/ownCloud/Settings/PurchasesSettingsSection.swift b/ownCloud/Settings/PurchasesSettingsSection.swift new file mode 100644 index 000000000..e2bf539a5 --- /dev/null +++ b/ownCloud/Settings/PurchasesSettingsSection.swift @@ -0,0 +1,63 @@ +// +// PurchasesSettingsSection.swift +// ownCloud +// +// Created by Felix Schwarz on 14.01.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +class PurchasesSettingsSection: SettingsSection { + // MARK: - More Settings Cells + + private var purchasesRow: StaticTableViewRow? + private var transactionsRow: StaticTableViewRow? + + override init(userDefaults: UserDefaults) { + super.init(userDefaults: userDefaults) + + self.headerTitle = "In-App Purchases".localized + self.identifier = "settings-purchases-section" + + createRows() + updateUI() + } + + // MARK: - Creation of the rows + private func createRows() { + purchasesRow = StaticTableViewRow(rowWithAction: { (row, _) in + row.viewController?.navigationController?.pushViewController(LicenseInAppProductListViewController(), animated: true) + }, title: "Pro Features".localized, accessoryType: .disclosureIndicator, identifier: "pro-features") + + transactionsRow = StaticTableViewRow(rowWithAction: { (row, _) in + row.viewController?.navigationController?.pushViewController(LicenseTransactionsViewController(), animated: true) + }, title: "Purchases".localized, accessoryType: .disclosureIndicator, identifier: "Purchases") + } + + // MARK: - Update UI + func updateUI() { + var rows : [StaticTableViewRow] = [] + + if let purchasesRow = purchasesRow { + rows.append(purchasesRow) + } + + if let transactionsRow = transactionsRow { + rows.append(transactionsRow) + } + + add(rows: rows) + } +} diff --git a/ownCloud/Settings/SettingsViewController.swift b/ownCloud/Settings/SettingsViewController.swift index 5bed47c7a..cdc99490e 100644 --- a/ownCloud/Settings/SettingsViewController.swift +++ b/ownCloud/Settings/SettingsViewController.swift @@ -31,6 +31,11 @@ class SettingsViewController: StaticTableViewController { self.addSection(DisplaySettingsSection(userDefaults: userDefaults)) self.addSection(MediaFilesSettingsSection(userDefaults: userDefaults)) self.addSection(MediaUploadSettingsSection(userDefaults: userDefaults)) + + if #available(iOS 13, *) { + self.addSection(PurchasesSettingsSection(userDefaults: userDefaults)) + } + self.addSection(MoreSettingsSection(userDefaults: userDefaults)) } } diff --git a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift index e92ce158a..2b9f1b928 100644 --- a/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift +++ b/ownCloud/Tasks/InstantMediaUploadTaskExtension.swift @@ -56,7 +56,7 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { guard let userDefaults = OCAppIdentity.shared.userDefaults else { return } var photoAssets = [PHAsset]() - + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching images created after \(String(describing: userDefaults.instantUploadPhotosAfter))") // Add photo assets @@ -78,7 +78,7 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { } var videoAssets = [PHAsset]() - + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching videos created after \(String(describing: userDefaults.instantUploadVideosAfter))") // Add video assets @@ -137,7 +137,7 @@ class InstantMediaUploadTaskExtension : ScheduledTaskAction { let sort = NSSortDescriptor(key: "modificationDate", ascending: true) fetchOptions.sortDescriptors = [sort] - + Log.debug(tagged: ["INSTANT_MEDIA_UPLOAD"], "Fetching assets with options \(fetchOptions.debugDescription)") return PHAsset.fetchAssets(in: cameraRoll, options: fetchOptions) diff --git a/ownCloud/Theming/NSObject+ThemeApplication.swift b/ownCloud/Theming/NSObject+ThemeApplication.swift index bfa3dc143..9ce8190d3 100644 --- a/ownCloud/Theming/NSObject+ThemeApplication.swift +++ b/ownCloud/Theming/NSObject+ThemeApplication.swift @@ -36,6 +36,8 @@ enum ThemeItemStyle { case bigTitle case bigMessage + + case purchase } enum ThemeItemState { @@ -77,6 +79,9 @@ extension NSObject { themeButton.themeColorCollection = collection.neutralColors themeButton.titleLabel?.font = UIFont.systemFont(ofSize: 34) + case .purchase: + themeButton.themeColorCollection = collection.purchaseColors + default: themeButton.themeColorCollection = collection.lightBrandColors.filledColorPairCollection } diff --git a/ownCloud/Theming/ThemeCollection.swift b/ownCloud/Theming/ThemeCollection.swift index 0e6ffbeaf..866f965a8 100644 --- a/ownCloud/Theming/ThemeCollection.swift +++ b/ownCloud/Theming/ThemeCollection.swift @@ -109,6 +109,8 @@ class ThemeCollection : NSObject { @objc var neutralColors : ThemeColorPairCollection @objc var destructiveColors : ThemeColorPairCollection + @objc var purchaseColors : ThemeColorPairCollection + // MARK: - Label colors @objc var informativeColor: UIColor @objc var successColor: UIColor @@ -202,6 +204,9 @@ class ThemeCollection : NSObject { self.neutralColors = lightBrandColors.filledColorPairCollection self.destructiveColors = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: UIColor.red)) + self.purchaseColors = ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: lightBrandColors.labelColor, background: lightBrandColor)) + self.purchaseColors.disabled.background = self.purchaseColors.disabled.background.greyscale() + self.tintColor = self.lightBrandColor // Table view diff --git a/ownCloud/Theming/UI/ThemeButton.swift b/ownCloud/Theming/UI/ThemeButton.swift index 4e65baa42..27b1d89d8 100644 --- a/ownCloud/Theming/UI/ThemeButton.swift +++ b/ownCloud/Theming/UI/ThemeButton.swift @@ -78,18 +78,45 @@ class ThemeButton : UIButton { override var intrinsicContentSize: CGSize { var intrinsicContentSize = super.intrinsicContentSize - intrinsicContentSize.width += 30 - intrinsicContentSize.height += 10 + intrinsicContentSize.width += buttonHorizontalPadding + intrinsicContentSize.height += buttonVerticalPadding return (intrinsicContentSize) } private func styleButton() { - self.layer.cornerRadius = 8 - self.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline) + adjustCornerRadius() + self.titleLabel?.font = buttonFont self.titleLabel?.adjustsFontForContentSizeCategory = true } + var buttonFont : UIFont = UIFont.preferredFont(forTextStyle: .headline) + var buttonHorizontalPadding : CGFloat = 30 { + didSet { + invalidateIntrinsicContentSize() + } + } + var buttonVerticalPadding : CGFloat = 10 { + didSet { + invalidateIntrinsicContentSize() + } + } + var buttonCornerRadius : CGFloat = 5 { + didSet { + adjustCornerRadius() + } + } + + func adjustCornerRadius() { + self.layer.cornerRadius = (buttonCornerRadius < 0) ? bounds.size.height/2 : buttonCornerRadius + } + + override var bounds: CGRect { + didSet { + self.adjustCornerRadius() + } + } + override init(frame: CGRect) { super.init(frame: frame) styleButton() diff --git a/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift b/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift index 051c86124..6ff6ee925 100644 --- a/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift +++ b/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift @@ -70,7 +70,7 @@ final class CardPresentationController: UIPresentationController, Themeable { } private var windowFrame: CGRect { - if let window = UIApplication.shared.currentWindow() as? UIWindow { + if let window = UIApplication.shared.currentWindow() { return window.bounds } else { return UIScreen.main.bounds diff --git a/ownCloud/UI Elements/StaticTableViewRow.swift b/ownCloud/UI Elements/StaticTableViewRow.swift index 21ba3f30e..5611729fc 100644 --- a/ownCloud/UI Elements/StaticTableViewRow.swift +++ b/ownCloud/UI Elements/StaticTableViewRow.swift @@ -702,6 +702,10 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { } } + @objc func actionTriggered(_ sender: UIView) { + action?(self, sender) + } + // MARK: - Deinit deinit { diff --git a/ownCloud/UIKit Extensions/UIAlertController+OCIssue.swift b/ownCloud/UIKit Extensions/UIAlertController+OCIssue.swift index d762cccc5..267cf6b7f 100644 --- a/ownCloud/UIKit Extensions/UIAlertController+OCIssue.swift +++ b/ownCloud/UIKit Extensions/UIAlertController+OCIssue.swift @@ -52,10 +52,12 @@ extension UIAlertController { } convenience init(with title: String, message: String, okLabel: String = "OK".localized, action: (() -> Void)? = nil) { - self.init(title: title, message: message, preferredStyle: .alert) + self.init(title: title, message: message, preferredStyle: UIDevice.current.isIpad() ? .alert : .actionSheet) + let okAction: UIAlertAction = UIAlertAction(title: okLabel, style: .default, handler: { (_) in action?() }) + self.addAction(okAction) } } diff --git a/ownCloud/Window/OpenItemUserActivity.swift b/ownCloud/Window/OpenItemUserActivity.swift index cf162db47..061570627 100644 --- a/ownCloud/Window/OpenItemUserActivity.swift +++ b/ownCloud/Window/OpenItemUserActivity.swift @@ -31,7 +31,7 @@ class OpenItemUserActivity : NSObject { var openItemUserActivity: NSUserActivity { let userActivity = NSUserActivity(activityType: ownCloudOpenItemActivityType) userActivity.title = ownCloudOpenItemPath - userActivity.userInfo = [ownCloudOpenItemUuidKey: item.localID, ownCloudOpenAccountAccountUuidKey : bookmark.uuid.uuidString] + userActivity.userInfo = [ownCloudOpenItemUuidKey: item.localID as Any, ownCloudOpenAccountAccountUuidKey : bookmark.uuid.uuidString] return userActivity } diff --git a/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.h b/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.h new file mode 100644 index 000000000..f6c01e87d --- /dev/null +++ b/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.h @@ -0,0 +1,30 @@ +// +// OCCore+LicenseEnvironment.h +// ownCloudApp +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseEnvironment.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCCore (LicenseEnvironment) + +@property(strong,readonly) OCLicenseEnvironment *licenseEnvironment; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m b/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m new file mode 100644 index 000000000..d31b12c3a --- /dev/null +++ b/ownCloudAppFramework/Licensing/Core Integration/OCCore+LicenseEnvironment.m @@ -0,0 +1,34 @@ +// +// OCCore+LicenseEnvironment.m +// ownCloudApp +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCCore+LicenseEnvironment.h" + +@implementation OCCore (LicenseEnvironment) + +- (OCLicenseEnvironment *)licenseEnvironment +{ + OCLicenseEnvironment *environment = nil; + + environment = [OCLicenseEnvironment environmentWithIdentifier:nil hostname:self.bookmark.url.host certificate:self.bookmark.certificate attributes:nil]; + environment.bookmarkUUID = self.bookmark.uuid; + environment.core = self; + + return (environment); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.h b/ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.h new file mode 100644 index 000000000..92e0071e4 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.h @@ -0,0 +1,53 @@ +// +// OCLicenseEntitlement.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OCLicenseProvider; +@class OCLicenseEnvironment; + +@interface OCLicenseEntitlement : NSObject + ++ (instancetype)entitlementWithIdentifier:(nullable OCLicenseEntitlementIdentifier)identifier forProduct:(OCLicenseProductIdentifier)productIdentifier type:(OCLicenseType)type valid:(BOOL)valid expiryDate:(nullable NSDate *)expiryDate applicability:(nullable OCLicenseEntitlementEnvironmentApplicableRule)applicability; + +#pragma mark - Metadata +@property(nullable,strong) OCLicenseEntitlementIdentifier identifier; //!< (optional) identifier uniquely identifying this license entitlement +@property(weak) OCLicenseProvider *provider; //!< Provider from which this entitlement originated + +#pragma mark - Product info +@property(strong) OCLicenseProductIdentifier productIdentifier; //!< Identifiers of the product targeted by this entitlement + +#pragma mark - Payload +@property(assign) OCLicenseType type; + +@property(nonatomic,assign) BOOL valid; //!< If the entitlement is currently valid (i.e. has not expired) +@property(nullable,strong) NSDate *expiryDate; //!< Date the entitlement expires - or nil if it doesn't expire + +@property(nullable,strong,nonatomic) NSDate *nextStatusChangeDate; //!< Date the entitlement should next be checked for changes - or nil if it doesn't need to be checked + +@property(nullable,strong) OCLicenseEntitlementEnvironmentApplicableRule environmentApplicableRule; //!< If provided, a rule (as string) in NSPredicate notation to check an OCLicenseEnvironment for applicability. If nil, the entitlement's applicability is not dependent on the environment. +- (BOOL)isApplicableInEnvironment:(OCLicenseEnvironment *)environment; //!< Returns YES if the entitlement is applicable in the provided environment + +- (OCLicenseAuthorizationStatus)authorizationStatusInEnvironment:(OCLicenseEnvironment *)environment; //!< Computes and returns the current authorization status for the respective environment based on .valid, .expiryDate and -isApplicableInEnvironment: + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.m b/ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.m new file mode 100644 index 000000000..914457df6 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Entitlement/OCLicenseEntitlement.m @@ -0,0 +1,109 @@ +// +// OCLicenseEntitlement.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseEntitlement.h" + +@implementation OCLicenseEntitlement + ++ (instancetype)entitlementWithIdentifier:(nullable OCLicenseEntitlementIdentifier)identifier forProduct:(OCLicenseProductIdentifier)productIdentifier type:(OCLicenseType)type valid:(BOOL)valid expiryDate:(nullable NSDate *)expiryDate applicability:(nullable OCLicenseEntitlementEnvironmentApplicableRule)applicability +{ + OCLicenseEntitlement *entitlement = [self new]; + + entitlement.identifier = identifier; + entitlement.productIdentifier = productIdentifier; + entitlement.type = type; + entitlement.valid = valid; + entitlement.expiryDate = expiryDate; + entitlement.environmentApplicableRule = applicability; + + return (entitlement); +} + +- (BOOL)valid +{ + // Check expiry date + if (_valid && (self.expiryDate != nil) && ([self.expiryDate timeIntervalSinceNow] < 0)) + { + // Entitlement has expired => no longer valid + return (NO); + } + + // Return the value for .valid that's been set + return (_valid); +} + +- (BOOL)isApplicableInEnvironment:(OCLicenseEnvironment *)environment +{ + if (self.environmentApplicableRule != nil) + { + if (environment == nil) + { + // Not applicable to any environment if there's an environment applicability rule but no environment to check against + return (NO); + } + else + { + NSPredicate *predicate = [NSPredicate predicateWithFormat:self.environmentApplicableRule, nil]; + + return ([predicate evaluateWithObject:environment]); + } + } + + return (YES); +} + +- (NSDate *)nextStatusChangeDate +{ + if (_nextStatusChangeDate != nil) + { + return (_nextStatusChangeDate); + } + + return (self.expiryDate); +} + +- (OCLicenseAuthorizationStatus)authorizationStatusInEnvironment:(OCLicenseEnvironment *)environment +{ + // Check expiry date + if ((self.expiryDate != nil) && ([self.expiryDate timeIntervalSinceNow] < 0)) + { + // Entitlement has an expiry date that's in the past => expired + return (OCLicenseAuthorizationStatusExpired); + } + + // Check validity + if (self.valid) + { + // Check applicability + if ([self isApplicableInEnvironment:environment]) + { + // Entitlement is valid and applicable to environment => granted + return (OCLicenseAuthorizationStatusGranted); + } + } + + // No valid, non-expired and applicable + return (OCLicenseAuthorizationStatusDenied); +} + +- (NSString *)description +{ + return ([NSString stringWithFormat:@"<%@: %p, identifier: %@, productIdentifier: %@, type: %lu, valid: %d, expiryDate: %@, environmentApplicableRule: %@>", NSStringFromClass(self.class), self, _identifier, _productIdentifier, (unsigned long)_type, _valid, _expiryDate, _environmentApplicableRule]); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.h b/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.h new file mode 100644 index 000000000..6f073184d --- /dev/null +++ b/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.h @@ -0,0 +1,47 @@ +// +// OCLicenseEnvironment.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import + +#import "OCLicenseTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* OCLicenseEnvironmentAttributesKey; + +@interface OCLicenseEnvironment : NSObject + +@property(nullable,strong) OCLicenseEnvironmentIdentifier identifier; + +@property(nullable,weak) OCCore *core; +@property(nullable,strong,nonatomic) OCBookmarkUUID bookmarkUUID; +@property(nullable,strong,nonatomic) OCBookmark *bookmark; + +@property(nullable,strong) NSString *hostname; +@property(nullable,strong) OCCertificate *certificate; + +@property(nullable,strong) NSDictionary *attributes; + ++ (instancetype)environmentWithIdentifier:(nullable OCLicenseEnvironmentIdentifier)identifier hostname:(nullable NSString *)hostname certificate:(nullable OCCertificate *)certificate attributes:(nullable NSDictionary *)attributes; + ++ (instancetype)environmentWithBookmark:(OCBookmark *)bookmark; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m b/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m new file mode 100644 index 000000000..74003b661 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Environment/OCLicenseEnvironment.m @@ -0,0 +1,84 @@ +// +// OCLicenseEnvironment.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseEnvironment.h" + +@implementation OCLicenseEnvironment + ++ (instancetype)environmentWithIdentifier:(nullable OCLicenseEnvironmentIdentifier)identifier hostname:(nullable NSString *)hostname certificate:(nullable OCCertificate *)certificate attributes:(NSDictionary *)attributes +{ + OCLicenseEnvironment *environment = [self new]; + + environment.identifier = identifier; + environment.hostname = hostname; + environment.certificate = certificate; + environment.attributes = attributes; + + return (environment); +} + ++ (instancetype)environmentWithBookmark:(OCBookmark *)bookmark +{ + OCLicenseEnvironment *environment = [self new]; + + environment.identifier = bookmark.uuid.UUIDString; + environment.bookmarkUUID = bookmark.uuid; + environment.bookmark = bookmark; + environment.hostname = bookmark.url.host; + environment.certificate = bookmark.certificate; + + return (environment); +} + +- (OCBookmarkUUID)bookmarkUUID +{ + if (_bookmarkUUID == nil) + { + if (_bookmark.uuid != nil) + { + return (_bookmark.uuid); + } + + if (_core.bookmark.uuid != nil) + { + return (_core.bookmark.uuid); + } + } + + return (_bookmarkUUID); +} + +- (OCBookmark *)bookmark +{ + if (_bookmark == nil) + { + if (_core.bookmark != nil) + { + return (_core.bookmark); + } + + if (_bookmarkUUID != nil) + { + return ([OCBookmarkManager.sharedBookmarkManager bookmarkForUUID:_bookmarkUUID]); + } + } + + return (_bookmark); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.h b/ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.h new file mode 100644 index 000000000..ffa0747c0 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.h @@ -0,0 +1,47 @@ +// +// OCLicenseFeature.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" + +@class OCLicenseManager; +@class OCLicenseProduct; +@class OCLicenseEntitlement; + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseFeature : NSObject + +@property(nullable,weak) OCLicenseManager *manager; + +#pragma mark - Metadata +@property(strong,readonly) OCLicenseFeatureIdentifier identifier; //!< Identifier uniquely identifying a feature + +@property(nullable,strong,readonly) NSString *localizedName; //!< Localized name of the feature +@property(nullable,strong,readonly) NSString *localizedDescription; //!< Localized description of the feature + +#pragma mark - Ownership +@property(strong) NSHashTable *containedInProducts; //!< Products in which this feature is contained +@property(nullable,strong,nonatomic) NSArray *entitlements; //!< Array of entitlements relevant to this feature + ++ (instancetype)featureWithIdentifier:(OCLicenseFeatureIdentifier)identifier; ++ (instancetype)featureWithIdentifier:(OCLicenseFeatureIdentifier)identifier name:(nullable NSString *)localizedName description:(nullable NSString *)localizedDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.m b/ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.m new file mode 100644 index 000000000..38ad87c5a --- /dev/null +++ b/ownCloudAppFramework/Licensing/Feature/OCLicenseFeature.m @@ -0,0 +1,72 @@ +// +// OCLicenseFeature.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseFeature.h" +#import "OCLicenseProduct.h" + +@implementation OCLicenseFeature + ++ (instancetype)featureWithIdentifier:(OCLicenseFeatureIdentifier)identifier +{ + return ([[self alloc] initWithIdentifier:identifier name:nil description:nil]); +} + ++ (instancetype)featureWithIdentifier:(OCLicenseFeatureIdentifier)identifier name:(NSString *)localizedName description:(NSString *)localizedDescription +{ + return ([[self alloc] initWithIdentifier:identifier name:localizedName description:localizedDescription]); +} + +- (instancetype)initWithIdentifier:(OCLicenseFeatureIdentifier)identifier name:(NSString *)localizedName description:(NSString *)localizedDescription +{ + if ((self = [super init]) != nil) + { + _identifier = identifier; + _localizedName = localizedName; + _localizedDescription = localizedDescription; + } + + return (self); +} + +- (NSArray *)entitlements +{ + @synchronized(self) + { + if (_entitlements == nil) + { + NSMutableArray *entitlements = [NSMutableArray new]; + + for (OCLicenseProduct *product in self.containedInProducts) + { + if (product.entitlements != nil) + { + [entitlements addObjectsFromArray:product.entitlements]; + } + } + + if (entitlements.count > 0) + { + _entitlements = entitlements; + } + } + + return (_entitlements); + } +} + +@end diff --git a/ownCloudAppFramework/Licensing/Manager/OCLicenseManager+Internal.h b/ownCloudAppFramework/Licensing/Manager/OCLicenseManager+Internal.h new file mode 100644 index 000000000..6e0f42a1c --- /dev/null +++ b/ownCloudAppFramework/Licensing/Manager/OCLicenseManager+Internal.h @@ -0,0 +1,20 @@ +// +// OCLicenseManager+Internal.h +// ownCloud +// +// Created by Felix Schwarz on 11.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +#import "OCLicenseManager.h" +#import "OCLicenseEntitlement.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseManager (Internal) + +- (nullable NSArray *)_entitlementsForProduct:(OCLicenseProduct *)product; //!< Returns the entitlements covering the product + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Manager/OCLicenseManager.h b/ownCloudAppFramework/Licensing/Manager/OCLicenseManager.h new file mode 100644 index 000000000..44a89126c --- /dev/null +++ b/ownCloudAppFramework/Licensing/Manager/OCLicenseManager.h @@ -0,0 +1,74 @@ +// +// OCLicenseManager.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import +#import "OCLicenseTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OCLicenseFeature; +@class OCLicenseProvider; +@class OCLicenseProduct; +@class OCLicenseOffer; +@class OCLicenseObserver; +@class OCLicenseTransaction; + +@interface OCLicenseManager : NSObject + +@property(strong,nonatomic,class,readonly) OCLicenseManager *sharedLicenseManager; + +@property(strong,nonatomic,readonly) OCAsyncSequentialQueue *queue; + +#pragma mark - Feature/product registration +- (void)registerFeature:(OCLicenseFeature *)feature; //!< Register a feature with the license manager +- (void)registerProduct:(OCLicenseProduct *)product; //!< Register a product with the license manager + +#pragma mark - Feature/product resolution +- (nullable OCLicenseProduct *)productWithIdentifier:(OCLicenseProductIdentifier)productIdentifier; //!< Returns the product for the passed identifier - or nil if none with that identifier was found. +- (nullable OCLicenseFeature *)featureWithIdentifier:(OCLicenseFeatureIdentifier)featureIdentifier; //!< Returns the feature for the passed identifier - or nil if none with that identifier was found. + +- (nullable NSArray *)offersForFeature:(OCLicenseFeature *)feature; //!< Returns an array of offers for products containing that feature, sorted by price. +- (nullable NSArray *)offersForProduct:(OCLicenseProduct *)product; //!< Returns an array of offers for the product, sorted by price. + +- (nullable NSArray *)featuresWithOffers:(BOOL)withOffers; //!< Returns an array of features for which offers are available + +#pragma mark - Provider management +- (void)addProvider:(OCLicenseProvider *)provider; //!< Add an entitlement and offer provider to the license manager +- (void)removeProvider:(OCLicenseProvider *)provider; //!< Remove an entitlement and offer provider from the license manager +- (nullable OCLicenseProvider *)providerForIdentifier:(OCLicenseProviderIdentifier)providerIdentifier; //!< Retrieve a provider by its identifier + +#pragma mark - Observation +- (OCLicenseObserver *)observeProducts:(nullable NSArray *)productIdentifiers features:(nullable NSArray *)featureIdentifiers inEnvironment:(OCLicenseEnvironment *)environment withOwner:(nullable id)owner updateHandler:(OCLicenseObserverAuthorizationStatusUpdateHandler)updateHandler; //!< Starts observing the authorization status of the products and features identified by their respective identifiers, in the passed environment. The passed .updateHandler will be called whenever the authorization status changes. An owner to which only a weak reference is stored can be passed for convenience. If the owner is deallocated, the observation will stop automatically. +- (OCLicenseObserver *)observeOffersForProducts:(nullable NSArray *)productIdentifiers features:(nullable NSArray *)featureIdentifiers withOwner:(nullable id)owner updateHandler:(OCLicenseObserverOffersUpdateHandler)updateHandler; //!< Starts observing offers covering the provided products and features. The passed .updateHandler will be called whenever the offers change. An owner to which only a weak reference is stored can be passed for convenience. If the owner is deallocated, the observation will stop automatically. + +- (void)stopObserver:(OCLicenseObserver *)observer; + +#pragma mark - Pending refresh tracking +- (void)performAfterCurrentlyPendingRefreshes:(dispatch_block_t)block; //!< Waits until all currently pending refreshes have been performed and then calls the block. Keep in mind pending refreshes are performed on the main thread, so you may not want to do any waiting or blocking for refreshes on the main thread, to avoid deadlocks. + +#pragma mark - One-off status info +- (OCLicenseAuthorizationStatus)authorizationStatusForFeature:(OCLicenseFeatureIdentifier)featureIdentifier inEnvironment:(OCLicenseEnvironment *)environment; +- (OCLicenseAuthorizationStatus)authorizationStatusForProduct:(OCLicenseProductIdentifier)productIdentifier inEnvironment:(OCLicenseEnvironment *)environment; + +#pragma mark - Transactions +- (void)retrieveAllTransactionsWithCompletionHandler:(void(^)(NSError * _Nullable error, NSArray *> * _Nullable transactionsByProvider))completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Manager/OCLicenseManager.m b/ownCloudAppFramework/Licensing/Manager/OCLicenseManager.m new file mode 100644 index 000000000..75f8cd614 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Manager/OCLicenseManager.m @@ -0,0 +1,861 @@ +// +// OCLicenseManager.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +#import "OCLicenseManager.h" +#import "OCLicenseManager+Internal.h" +#import "OCLicenseFeature.h" +#import "OCLicenseProduct.h" +#import "OCLicenseProvider.h" +#import "OCLicenseOffer.h" +#import "OCLicenseEntitlement.h" +#import "OCLicenseObserver.h" + +@interface OCLicenseManager () +{ + // Features + NSMutableArray *_features; + NSMutableDictionary *_featuresByIdentifier; + + // Products + NSMutableArray *_products; + NSMutableDictionary *_productsByIdentifier; + + BOOL _needsProductFeatureRewiring; + + // Providers + NSMutableArray *_providers; + + NSMutableSet *_offers; + NSMutableSet *_entitlements; + BOOL _needsRebuildFromProviders; + + // Observers + NSMapTable *> *_observersByOwner; + NSHashTable *_observers; + BOOL _needsObserverUpdate; + + NSDate *_nextEarliestExpectedChangeDate; +} + +@end + +@implementation OCLicenseManager + ++ (OCLicenseManager *)sharedLicenseManager +{ + static dispatch_once_t onceToken; + static OCLicenseManager *sharedLicenseManager; + + dispatch_once(&onceToken, ^{ + sharedLicenseManager = [OCLicenseManager new]; + }); + + return (sharedLicenseManager); +} + +- (instancetype)init +{ + if ((self = [super init]) != nil) + { + _queue = [OCAsyncSequentialQueue new]; + + _features = [NSMutableArray new]; + _featuresByIdentifier = [NSMutableDictionary new]; + + _products = [NSMutableArray new]; + _productsByIdentifier = [NSMutableDictionary new]; + + _providers = [NSMutableArray new]; + + _offers = [NSMutableSet new]; + _entitlements = [NSMutableSet new]; + + _observersByOwner = [NSMapTable weakToStrongObjectsMapTable]; + _observers = [NSHashTable weakObjectsHashTable]; + } + + return (self); +} + +- (void)dealloc +{ + // Remove observers + for (OCLicenseProvider *provider in _providers) + { + [provider removeObserver:self forKeyPath:@"offers" context:(__bridge void *)self]; + [provider removeObserver:self forKeyPath:@"entitlements" context:(__bridge void *)self]; + } + + _providers = nil; +} + +#pragma mark - Feature/product registration +- (void)registerFeature:(OCLicenseFeature *)feature +{ + @synchronized(self) + { + [_features addObject:feature]; + _featuresByIdentifier[feature.identifier] = feature; + + feature.manager = self; + + [self setNeedsProductFeatureRewiring]; + } +} + +- (void)registerProduct:(OCLicenseProduct *)product +{ + @synchronized(self) + { + [_products addObject:product]; + _productsByIdentifier[product.identifier] = product; + + product.manager = self; + + [self setNeedsProductFeatureRewiring]; + } +} + +#pragma mark - Feature/product resolution +- (nullable OCLicenseProduct *)productWithIdentifier:(OCLicenseProductIdentifier)productIdentifier +{ + OCLicenseProduct *product = nil; + + @synchronized (self) + { + product = _productsByIdentifier[productIdentifier]; + } + + return (product); +} + +- (nullable OCLicenseFeature *)featureWithIdentifier:(OCLicenseFeatureIdentifier)featureIdentifier +{ + OCLicenseFeature *feature = nil; + + @synchronized (self) + { + feature = _featuresByIdentifier[featureIdentifier]; + } + + return (feature); +} + +- (nullable NSArray *)_orderedOffersFromOfferSet:(NSMutableSet *)offerSet +{ + return ([offerSet sortedArrayUsingDescriptors:@[ + [NSSortDescriptor sortDescriptorWithKey:@"type" ascending:YES], + [NSSortDescriptor sortDescriptorWithKey:@"price" ascending:YES] + ]]); +} + +- (nullable NSArray *)offersForFeature:(OCLicenseFeature *)feature +{ + NSMutableSet *offerSet = [NSMutableSet new]; + + for (OCLicenseProduct *product in feature.containedInProducts) + { + NSArray *offersForProduct; + + if ((offersForProduct = [self offersForProduct:product]) != nil) + { + [offerSet addObjectsFromArray:offersForProduct]; + } + } + + return ([self _orderedOffersFromOfferSet:offerSet]); +} + +- (nullable NSArray *)offersForProduct:(OCLicenseProduct *)product +{ + NSMutableSet *offerSet = [NSMutableSet new]; + + @synchronized(self) + { + for (OCLicenseOffer *offer in _offers) + { + if ((offer.productIdentifier != nil) && [offer.productIdentifier isEqual:product.identifier]) + { + [offerSet addObject:offer]; + } + } + } + + return ([self _orderedOffersFromOfferSet:offerSet]); +} + +- (nullable NSArray *)featuresWithOffers:(BOOL)withOffers +{ + NSMutableArray *features = [NSMutableArray new]; + + @synchronized (self) + { + for (OCLicenseFeature *feature in _features) + { + if (!withOffers || (withOffers && ([self offersForFeature:feature].count > 0))) + { + [features addObject:feature]; + } + } + } + + [features sortUsingDescriptors:@[ + [NSSortDescriptor sortDescriptorWithKey:@"localizedName" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)] + ]]; + + return (features); +} + +#pragma mark - Product/feature rewiring +- (void)setNeedsProductFeatureRewiring +{ + [self _setNeedsRun:&_needsProductFeatureRewiring async:^(OCLicenseManager *manager, dispatch_block_t completionHandler) { + [manager _rewireProductsAndFeaturesWithCompletionHandler:completionHandler]; + }]; +} + +- (void)_rewireProductsAndFeaturesWithCompletionHandler:(dispatch_block_t)completionHandler +{ + @synchronized(self) + { + NSMutableDictionary *> *productsByFeatureID = [NSMutableDictionary new]; + + for (OCLicenseProduct *product in _products) + { + NSMutableArray *features = [NSMutableArray new]; + + for (OCLicenseFeatureIdentifier featureIdentifier in product.contents) + { + OCLicenseFeature *feature; + + if ((feature = _featuresByIdentifier[featureIdentifier]) != nil) + { + [features addObject:feature]; + } + + // Build list of products for each feature + NSHashTable *productsForFeature = nil; + + if ((productsForFeature = productsByFeatureID[featureIdentifier]) == nil) + { + productsForFeature = [NSHashTable weakObjectsHashTable]; + productsByFeatureID[featureIdentifier] = productsForFeature; + } + + [productsForFeature addObject:product]; + } + + // Set features for product + product.features = (features.count > 0) ? features : nil; + } + + // Set products for all features + for (OCLicenseFeature *feature in _features) + { + feature.containedInProducts = productsByFeatureID[feature.identifier]; + } + } + + completionHandler(); +} + +#pragma mark - Provider management +- (void)addProvider:(OCLicenseProvider *)provider +{ + @synchronized(self) + { + [_providers addObject:provider]; + [provider addObserver:self forKeyPath:@"offers" options:0 context:(__bridge void *)self]; + [provider addObserver:self forKeyPath:@"entitlements" options:0 context:(__bridge void *)self]; + + provider.manager = self; + + [provider startProvidingWithCompletionHandler:^(OCLicenseProvider *provider, NSError * _Nullable error) { + if (error != nil) + { + OCLogError(@"Error starting license provider %@: %@", provider, error); + } + }]; + + [self setNeedsRebuildFromProviders]; + } +} + +- (void)removeProvider:(OCLicenseProvider *)provider +{ + @synchronized(self) + { + [provider removeObserver:self forKeyPath:@"offers" context:(__bridge void *)self]; + [provider removeObserver:self forKeyPath:@"entitlements" context:(__bridge void *)self]; + + [provider stopProvidingWithCompletionHandler:^(OCLicenseProvider *provider, NSError * _Nullable error) { + if (error != nil) + { + OCLogError(@"Error stopping license provider %@: %@", provider, error); + } + }]; + + provider.manager = nil; + + [_providers removeObject:provider]; + + [self setNeedsRebuildFromProviders]; + } +} + +- (nullable OCLicenseProvider *)providerForIdentifier:(OCLicenseProviderIdentifier)providerIdentifier +{ + OCLicenseProvider *result = nil; + + @synchronized(self) + { + for (OCLicenseProvider *provider in _providers) + { + if ([provider.identifier isEqual:providerIdentifier]) + { + result = provider; + break; + } + } + } + + return (result); +} + +#pragma mark - Transactions +- (void)retrieveAllTransactionsWithCompletionHandler:(void(^)(NSError *error, NSArray *> *transactionsByProvider))completionHandler +{ + dispatch_group_t retrieveGroup = dispatch_group_create(); + __block NSMutableArray *> *transactionsByProvider = [NSMutableArray new]; + __block NSError *allError = nil; + + @synchronized(self) + { + for (OCLicenseProvider *provider in _providers) + { + dispatch_group_enter(retrieveGroup); + + [provider retrieveTransactionsWithCompletionHandler:^(NSError * _Nonnull error, NSArray * _Nullable transactions) { + if (error != nil) + { + allError = error; + } + + if (transactions != nil) + { + [transactionsByProvider addObject:transactions]; + } + + dispatch_group_leave(retrieveGroup); + }]; + } + } + + dispatch_group_notify(retrieveGroup, dispatch_get_main_queue(), ^{ + completionHandler(allError, transactionsByProvider); + }); +} + +#pragma mark - Observation +- (void)_addObserver:(OCLicenseObserver *)observer withOwner:(id)owner +{ + @synchronized(self) + { + NSMutableArray *observers; + + if ((observers = [_observersByOwner objectForKey:owner]) == nil) + { + observers = [NSMutableArray new]; + [_observersByOwner setObject:observers forKey:owner]; + } + [observers addObject:observer]; + + [_observers addObject:observer]; + + [self _updateObserver:observer]; + } +} + +- (OCLicenseObserver *)observeProducts:(nullable NSArray *)productIdentifiers features:(nullable NSArray *)featureIdentifiers inEnvironment:(OCLicenseEnvironment *)environment withOwner:(nullable id)owner updateHandler:(OCLicenseObserverAuthorizationStatusUpdateHandler)updateHandler +{ + OCLicenseObserver *observer = [OCLicenseObserver new]; + + if (owner == nil) { owner = self; } + + observer.products = productIdentifiers; + observer.features = featureIdentifiers; + observer.environment = environment; + observer.owner = owner; + observer.statusUpdateHandler = updateHandler; + + [self _addObserver:observer withOwner:owner]; + + return (observer); +} + +- (OCLicenseObserver *)observeOffersForProducts:(nullable NSArray *)productIdentifiers features:(nullable NSArray *)featureIdentifiers withOwner:(nullable id)owner updateHandler:(OCLicenseObserverOffersUpdateHandler)updateHandler +{ + OCLicenseObserver *observer = [OCLicenseObserver new]; + + if (owner == nil) { owner = self; } + + observer.products = productIdentifiers; + observer.features = featureIdentifiers; + observer.owner = owner; + observer.offersUpdateHandler = updateHandler; + + [self _addObserver:observer withOwner:owner]; + + return (observer); +} + +- (void)stopObserver:(OCLicenseObserver *)observer +{ + @synchronized(self) + { + [[_observersByOwner objectForKey:observer.owner] removeObject:observer]; + [_observers removeObject:observer]; + } +} + +- (void)setNeedsObserverUpdate +{ + [self _setNeedsRun:&_needsObserverUpdate async:^(OCLicenseManager *manager, dispatch_block_t completionHandler) { + [manager _updateObserversWithCompletionHandler:completionHandler]; + }]; +} + +- (void)_updateObserversWithCompletionHandler:(dispatch_block_t)completionHandler +{ + @synchronized(self) + { + // Reset entitlements in products and features + // (will be rebuilt lazely, with features depending on products, + // so products need to be reset first) + for (OCLicenseFeature *product in _products) + { + product.entitlements = nil; + } + + for (OCLicenseFeature *feature in _features) + { + feature.entitlements = nil; + } + + // After reset: update observers + for (OCLicenseObserver *observer in _observers) + { + [self _updateObserver:observer]; + } + } + + completionHandler(); +} + +- (OCLicenseAuthorizationStatus)_authorizationStatusForEntitlements:(NSArray *)entitlements inEnvironment:(OCLicenseEnvironment *)environment +{ + OCLicenseAuthorizationStatus summaryAuthStatus = OCLicenseAuthorizationStatusUnknown; + + OCLogDebug(@"Determining authorization status with entitlements: %@", entitlements); + + // No entitlements => denied + if (entitlements.count == 0) + { + return (OCLicenseAuthorizationStatusDenied); + } + + // Find greatest authorization status among entitlements and return it as summary value + for (OCLicenseEntitlement *entitlement in entitlements) + { + OCLicenseAuthorizationStatus authStatus; + + authStatus = [entitlement authorizationStatusInEnvironment:environment]; + + if (authStatus > summaryAuthStatus) + { + summaryAuthStatus = authStatus; + } + } + + return (summaryAuthStatus); +} + +- (void)_updateObserver:(OCLicenseObserver *)observer +{ + [self _updateAuthorizationStatusForObserver:observer]; + [self _updateOffersForObserver:observer]; +} + +- (void)_updateOffersForObserver:(OCLicenseObserver *)observer +{ + // Collect products + NSMutableSet *productIdentifiers = [NSMutableSet new]; + + // From observed products + for (OCLicenseProductIdentifier productIdentifier in observer.products) + { + OCLicenseProduct *product; + + if ((product = [self productWithIdentifier:productIdentifier]) != nil) + { + [productIdentifiers addObject:productIdentifier]; + } + } + + // From observed features + for (OCLicenseFeatureIdentifier featureIdentifier in observer.features) + { + OCLicenseFeature *feature; + + if ((feature = [self featureWithIdentifier:featureIdentifier]) != nil) + { + for (OCLicenseProduct *product in feature.containedInProducts) + { + if (product.identifier != nil) + { + [productIdentifiers addObject:product.identifier]; + } + } + } + } + + // Find matching offers + NSMutableArray *offers = [NSMutableArray new]; + + for (OCLicenseOffer *offer in _offers) + { + if ([productIdentifiers containsObject:offer.productIdentifier]) + { + [offers addObject:offer]; + } + } + + observer.offers = offers; +} + +- (void)_updateAuthorizationStatusForObserver:(OCLicenseObserver *)observer +{ + OCLicenseAuthorizationStatus summaryAuthStatus = OCLicenseAuthorizationStatusGranted; + + // Evaluate entitlements from products + for (OCLicenseProductIdentifier productIdentifier in observer.products) + { + OCLicenseProduct *product; + + if ((product = [self productWithIdentifier:productIdentifier]) != nil) + { + // Product found => compute authorization status + OCLicenseAuthorizationStatus authStatus = [self _authorizationStatusForEntitlements:product.entitlements inEnvironment:observer.environment]; + + if (authStatus < summaryAuthStatus) + { + summaryAuthStatus = authStatus; + } + } + else + { + // Product not found => authorization denied + summaryAuthStatus = OCLicenseAuthorizationStatusDenied; + break; + } + } + + // Evaluate entitlements from features + if (summaryAuthStatus != OCLicenseAuthorizationStatusDenied) + { + for (OCLicenseFeatureIdentifier featureIdentifier in observer.features) + { + OCLicenseFeature *feature; + + if ((feature = [self featureWithIdentifier:featureIdentifier]) != nil) + { + // Feature found => compute authorization status + OCLicenseAuthorizationStatus authStatus = [self _authorizationStatusForEntitlements:feature.entitlements inEnvironment:observer.environment]; + + if (authStatus < summaryAuthStatus) + { + summaryAuthStatus = authStatus; + } + } + else + { + // Feature not found => authorization denied + summaryAuthStatus = OCLicenseAuthorizationStatusDenied; + break; + } + } + } + + // Update observer's authorization status (which will notify its update handler as needed) + observer.authorizationStatus = summaryAuthStatus; +} + +#pragma mark - Change observation +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == (__bridge void *)self) + { + OCLicenseProvider *provider; + + if ((provider = OCTypedCast(object, OCLicenseProvider)) != nil) + { + if ([keyPath isEqualToString:@"offers"] || [keyPath isEqualToString:@"entitlements"]) + { + [self setNeedsRebuildFromProviders]; + } + } + } +} + +#pragma mark - One-off status info +- (OCLicenseAuthorizationStatus)authorizationStatusForFeature:(OCLicenseFeatureIdentifier)featureIdentifier inEnvironment:(OCLicenseEnvironment *)environment +{ + OCLicenseAuthorizationStatus authStatus = OCLicenseAuthorizationStatusUnknown; + OCLicenseFeature *feature; + + if ((feature = [self featureWithIdentifier:featureIdentifier]) != nil) + { + // Feature found => compute authorization status + authStatus = [self _authorizationStatusForEntitlements:feature.entitlements inEnvironment:environment]; + } + + OCLogDebug(@"Returning authorizationStatus %lu for feature %@ in environment %@…", (unsigned long)authStatus, featureIdentifier, environment); + + return (authStatus); +} + +- (OCLicenseAuthorizationStatus)authorizationStatusForProduct:(OCLicenseProductIdentifier)productIdentifier inEnvironment:(OCLicenseEnvironment *)environment +{ + OCLicenseAuthorizationStatus authStatus = OCLicenseAuthorizationStatusUnknown; + OCLicenseProduct *product; + + if ((product = [self productWithIdentifier:productIdentifier]) != nil) + { + // Product found => compute authorization status + authStatus = [self _authorizationStatusForEntitlements:product.entitlements inEnvironment:environment]; + } + + OCLogDebug(@"Returning authorizationStatus %lu for produt %@ in environment %@…", (unsigned long)authStatus, productIdentifier, environment); + + return (authStatus); +} + +#pragma mark - Updates +- (void)setNeedsRebuildFromProviders +{ + [self _setNeedsRun:&_needsRebuildFromProviders async:^(OCLicenseManager *manager, dispatch_block_t completionHandler) { + [manager _rebuildFromProvidersWithCompletionHandler:completionHandler]; + }]; +} + +- (void)_rebuildFromProvidersWithCompletionHandler:(dispatch_block_t)completionHandler +{ + OCLogDebug(@"Rebuilding from providers…"); + + @synchronized(self) + { + NSMutableSet *newEntitlements = [NSMutableSet new]; + NSMutableSet *newOffers = [NSMutableSet new]; + BOOL offersUpdated = NO, entitlementsUpdated = NO; + + for (OCLicenseProvider *provider in _providers) + { + if (provider.entitlements.count > 0) + { + [newEntitlements addObjectsFromArray:provider.entitlements]; + } + + if (provider.offers.count > 0) + { + [newOffers addObjectsFromArray:provider.offers]; + } + } + + if (![_entitlements isEqualToSet:newEntitlements]) + { + // Entitlements were updated + + // Replace existing entitlements + [_entitlements setSet:newEntitlements]; + + entitlementsUpdated = YES; + } + + if (![_offers isEqualToSet:newOffers]) + { + // Offers were updated + + // Replace existing offers + [_offers setSet:newOffers]; + + offersUpdated = YES; + } + + if (entitlementsUpdated || offersUpdated) + { + // Update observers + [self setNeedsObserverUpdate]; + } + + + // Compute the earliest change date (if any) + NSDate *earliestExpectedChangeDate = nil; + + #define ConsiderAsEarliestChangeDate(dateVar,considerDate) if (considerDate != nil) \ + { \ + if ((dateVar == nil) || ((considerDate.timeIntervalSinceNow < dateVar.timeIntervalSinceNow) && (considerDate.timeIntervalSinceNow > 0))) \ + { \ + dateVar = considerDate; \ + } \ + } + + for (OCLicenseEntitlement *entitlement in newEntitlements) + { + ConsiderAsEarliestChangeDate(earliestExpectedChangeDate, entitlement.nextStatusChangeDate); + } + + for (OCLicenseOffer *offer in newOffers) + { + ConsiderAsEarliestChangeDate(earliestExpectedChangeDate, offer.fromDate); + ConsiderAsEarliestChangeDate(earliestExpectedChangeDate, offer.untilDate); + } + + // Trigger updates at earliestExpectedChangeDate if needed + if (![earliestExpectedChangeDate isEqual:_nextEarliestExpectedChangeDate]) + { + _nextEarliestExpectedChangeDate = earliestExpectedChangeDate; + + if (earliestExpectedChangeDate != nil) + { + NSTimeInterval timeIntervalSinceNow = earliestExpectedChangeDate.timeIntervalSinceNow; + + if (timeIntervalSinceNow > 0) + { + __weak OCLicenseManager *weakManager = self; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((timeIntervalSinceNow + 1.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + OCLicenseManager *strongManager; + + if ((strongManager = weakManager) != nil) + { + if ([strongManager->_nextEarliestExpectedChangeDate isEqual:earliestExpectedChangeDate]) // only perform this if it's still relevant + { + // Rebuild from providers so the earliestExpectedChangeDate gets updated + [strongManager setNeedsRebuildFromProviders]; + + // Update observers because their status may have changed + [strongManager setNeedsObserverUpdate]; + } + } + }); + } + } + } + + completionHandler(); + } +} + +#pragma mark - Update coalescation +- (void)_setNeedsRun:(BOOL *)inOutNeedsRun async:(void(^)(OCLicenseManager *manager, dispatch_block_t completionHandler))block +{ + BOOL triggerRun = NO; + + @synchronized(self) + { + if (!*inOutNeedsRun) + { + *inOutNeedsRun = YES; + + triggerRun = YES; + } + } + + if (triggerRun) + { + [_queue async:^(dispatch_block_t _Nonnull completionHandler) { + @synchronized(self) + { + if (*inOutNeedsRun) + { + *inOutNeedsRun = NO; + } + else + { + completionHandler(); + return; + } + } + + block(self, completionHandler); + }]; + } +} + +#pragma mark - Pending refresh tracking +- (void)performAfterCurrentlyPendingRefreshes:(dispatch_block_t)block +{ + OCLogDebug(@"Queuing block %@ for execution after completing pending refreshes…", block); + + [_queue async:^(dispatch_block_t _Nonnull completionHandler) { + block(); + completionHandler(); + }]; +} + +#pragma mark - Log tagging ++ (NSArray *)logTags +{ + return (@[@"Licensing", @"Manager"]); +} + +- (NSArray *)logTags +{ + return (@[@"Licensing", @"Manager"]); +} + +@end + +@implementation OCLicenseManager (Internal) + +- (nullable NSArray *)_entitlementsForProduct:(OCLicenseProduct *)product +{ + NSMutableArray *productEntitlements = nil; + + @synchronized(self) + { + for (OCLicenseEntitlement *entitlement in _entitlements) + { + if ([entitlement.productIdentifier isEqual:product.identifier]) + { + if (productEntitlements == nil) + { + productEntitlements = [NSMutableArray new]; + } + + [productEntitlements addObject:entitlement]; + } + } + } + + return (productEntitlements); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.h b/ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.h new file mode 100644 index 000000000..5b5431517 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.h @@ -0,0 +1,40 @@ +// +// OCLicenseObserver.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseObserver : NSObject + +@property(weak,nullable) OCLicenseEnvironment *environment; //!< The environment for which to observe authorization status +@property(weak,nullable) id owner; //!< The owner of the observer. If the owner is deallocated, the observer is automatically removed. + +@property(strong,nullable) NSArray *products; //!< Identifiers of the products to observe (need to be resolvable - or authorizationStatus will always be denied) +@property(strong,nullable) NSArray *features; //!< Identifiers of the features to observe (need to be resolvable - or authorizationStatus will always be denied) + +@property(assign,nonatomic) OCLicenseAuthorizationStatus authorizationStatus; //!< Combined authorization status of all .products and .features (== lowest authorization status determined among them). +@property(copy,nullable) OCLicenseObserverAuthorizationStatusUpdateHandler statusUpdateHandler; //!< Update handler block. Called whenever the authorizationStatus changes. + +@property(strong,nonatomic,nullable) NSArray *offers; //!< Offers covering any of the products or features specified +@property(copy,nullable) OCLicenseObserverOffersUpdateHandler offersUpdateHandler; //!< Update handler block. Called whenever the offers change. + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.m b/ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.m new file mode 100644 index 000000000..49f536ce2 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Manager/OCLicenseObserver.m @@ -0,0 +1,85 @@ +// +// OCLicenseObserver.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseObserver.h" + +@interface OCLicenseObserver () +{ + BOOL _didInitialUpdate; +} +@end + +@implementation OCLicenseObserver + +- (void)setAuthorizationStatus:(OCLicenseAuthorizationStatus)authorizationStatus +{ + BOOL isInitial = NO; + OCLicenseObserverAuthorizationStatusUpdateHandler updateHandler = nil; + + @synchronized(self) + { + if (_authorizationStatus != authorizationStatus) + { + _authorizationStatus = authorizationStatus; + + if ((updateHandler = self.statusUpdateHandler) != nil) + { + if (!_didInitialUpdate) + { + isInitial = YES; + _didInitialUpdate = YES; + } + } + } + } + + if (updateHandler != nil) + { + updateHandler(self, isInitial, authorizationStatus); + } +} + +- (void)setOffers:(NSArray *)offers +{ + BOOL isInitial = NO; + OCLicenseObserverOffersUpdateHandler updateHandler = nil; + + @synchronized(self) + { + if (![_offers isEqual:offers]) + { + _offers = offers; + + if ((updateHandler = self.offersUpdateHandler) != nil) + { + if (!_didInitialUpdate) + { + isInitial = YES; + _didInitialUpdate = YES; + } + } + } + } + + if (updateHandler != nil) + { + updateHandler(self, isInitial, offers); + } +} + +@end diff --git a/ownCloudAppFramework/Licensing/OCLicenseTypes.h b/ownCloudAppFramework/Licensing/OCLicenseTypes.h new file mode 100644 index 000000000..744b762ad --- /dev/null +++ b/ownCloudAppFramework/Licensing/OCLicenseTypes.h @@ -0,0 +1,59 @@ +// +// OCLicenseTypes.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +typedef NSString* OCLicenseProductIdentifier NS_TYPED_ENUM; +typedef NSString* OCLicenseFeatureIdentifier NS_TYPED_ENUM; +typedef NSString* OCLicenseProviderIdentifier NS_TYPED_ENUM; +typedef NSString* OCLicenseEnvironmentIdentifier; +typedef NSString* OCLicenseEntitlementIdentifier; +typedef NSString* OCLicenseOfferIdentifier; +typedef NSString* OCLicenseOfferGroupIdentifier; + +typedef NSString* OCLicenseOfferCommitOption NS_TYPED_ENUM; +typedef NSDictionary* OCLicenseOfferCommitOptions; + +typedef NSString* OCLicenseEntitlementEnvironmentApplicableRule; + +typedef NS_ENUM(NSUInteger, OCLicenseType) +{ + OCLicenseTypeNone, //!< NO license + OCLicenseTypeTrial, //!< Trial + OCLicenseTypeSubscription, //!< Subscription + OCLicenseTypePurchase //!< Regular purchase +}; + +typedef NS_ENUM(NSUInteger, OCLicenseAuthorizationStatus) +{ + OCLicenseAuthorizationStatusUnknown, //!< Status unknown + OCLicenseAuthorizationStatusDenied, //!< Authorization denied + OCLicenseAuthorizationStatusExpired, //!< Authorization expired, existed at some point in the past + OCLicenseAuthorizationStatusGranted //!< Authorization granted +}; + +@class OCLicenseEnvironment; +@class OCLicenseObserver; +@class OCLicenseOffer; + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^OCLicenseObserverAuthorizationStatusUpdateHandler)(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus); +typedef void(^OCLicenseObserverOffersUpdateHandler)(OCLicenseObserver * _Nonnull observer, BOOL isInitial, NSArray *offers); + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.h b/ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.h new file mode 100644 index 000000000..4c16ac9e3 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.h @@ -0,0 +1,56 @@ +// +// OCLicenseDuration.h +// ownCloud +// +// Created by Felix Schwarz on 04.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import + +typedef NS_ENUM(NSUInteger, OCLicenseDurationUnit) +{ + OCLicenseDurationUnitNone, + + OCLicenseDurationUnitDay, + OCLicenseDurationUnitWeek, + OCLicenseDurationUnitMonth, + OCLicenseDurationUnitYear +}; + +typedef NSInteger OCLicenseDurationLength; + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseDuration : NSObject + +@property(assign) OCLicenseDurationLength length; +@property(assign) OCLicenseDurationUnit unit; + +@property(nonatomic,readonly) NSTimeInterval duration; +@property(nonatomic,readonly) NSString *localizedDescription; + +- (instancetype)initWithUnit:(OCLicenseDurationUnit)unit length:(OCLicenseDurationLength)length; + +- (NSDate *)dateWithDurationAddedTo:(NSDate *)date; + +@end + +@interface SKProductSubscriptionPeriod (OCLicenseDuration) + +@property(readonly,nonatomic,nullable) OCLicenseDuration *licenseDuration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.m b/ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.m new file mode 100644 index 000000000..d381baed1 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Offer/OCLicenseDuration.m @@ -0,0 +1,166 @@ +// +// OCLicenseDuration.m +// ownCloud +// +// Created by Felix Schwarz on 04.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseDuration.h" + +@implementation OCLicenseDuration + +- (instancetype)initWithUnit:(OCLicenseDurationUnit)unit length:(OCLicenseDurationLength)length +{ + if ((self = [super init]) != nil) + { + _unit = unit; + _length = length; + } + + return (self); +} + +- (NSTimeInterval)duration +{ + switch (_unit) + { + case OCLicenseDurationUnitDay: + return (24 * 3600 * _length); + break; + + case OCLicenseDurationUnitWeek: + return (7 * 24 * 3600 * _length); + break; + + case OCLicenseDurationUnitMonth: + return (30 * 24 * 3600 * _length); + break; + + case OCLicenseDurationUnitYear: + return (365 * 24 * 3600 * _length); + break; + + case OCLicenseDurationUnitNone: + break; + } + + return (0); +} + +- (NSString *)localizedDescription +{ + NSString *format = nil; + + switch (_unit) + { + case OCLicenseDurationUnitDay: + if (_length == 1) + { + format = OCLocalized(@"day"); + } + else + { + format = OCLocalized(@"%lu days"); + } + break; + + case OCLicenseDurationUnitWeek: + if (_length == 1) + { + format = OCLocalized(@"week"); + } + else + { + format = OCLocalized(@"%lu weeks"); + } + break; + + case OCLicenseDurationUnitMonth: + if (_length == 1) + { + format = OCLocalized(@"month"); + } + else + { + format = OCLocalized(@"%lu months"); + } + break; + + case OCLicenseDurationUnitYear: + if (_length == 1) + { + format = OCLocalized(@"year"); + } + else + { + format = OCLocalized(@"%lu years"); + } + break; + + case OCLicenseDurationUnitNone: + break; + } + + if (format != nil) + { + return ([NSString stringWithFormat:format, _length]); + } + + return (nil); +} + +- (NSDate *)dateWithDurationAddedTo:(NSDate *)date +{ + // TODO: Create more sophisticated implementation that computes the _precise_ date, so that 1 Jan + 1 month = 1 Feb, 10 Feb 2019 + 1 year = 10 Feb 2020 + return ([date dateByAddingTimeInterval:self.duration]); +} + +@end + + +@implementation SKProductSubscriptionPeriod (OCLicenseDuration) + +- (nullable OCLicenseDuration *)licenseDuration +{ + OCLicenseDurationUnit unit = OCLicenseDurationUnitNone; + + switch (self.unit) + { + case SKProductPeriodUnitDay: + unit = OCLicenseDurationUnitDay; + break; + + case SKProductPeriodUnitWeek: + unit = OCLicenseDurationUnitWeek; + break; + + case SKProductPeriodUnitMonth: + unit = OCLicenseDurationUnitMonth; + break; + + case SKProductPeriodUnitYear: + unit = OCLicenseDurationUnitYear; + break; + } + + if (unit != OCLicenseDurationUnitNone) + { + return ([[OCLicenseDuration alloc] initWithUnit:unit length:self.numberOfUnits]); + } + + return (nil); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.h b/ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.h new file mode 100644 index 000000000..4b6c283bc --- /dev/null +++ b/ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.h @@ -0,0 +1,91 @@ +// +// OCLicenseOffer.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" +#import "OCLicenseDuration.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OCLicenseProvider; +@class OCLicenseDuration; +@class OCLicenseProduct; + +typedef void(^OCLicenseOfferCommitErrorHandler)(NSError * _Nullable error); +typedef void(^OCLicenseOfferCommitHandler)(OCLicenseOffer *offer, OCLicenseOfferCommitOptions _Nullable options, OCLicenseOfferCommitErrorHandler _Nullable errorHandler); + +typedef NS_ENUM(NSUInteger, OCLicenseOfferState) +{ + OCLicenseOfferStateUncommitted, //!< The offer has not been commited to (bought) by the user. + OCLicenseOfferStateUnavailable, //!< The offer is not available + OCLicenseOfferStateRedundant, //!< The user has not committed to (bought) the offer, but committed to (an)other offer(s) that also cover the entirety of the contents of this offer. It is therefore redundant. + + OCLicenseOfferStateInProgress, //!< The user is committing to (buying) the offer, but the commitment is still being processed. + + OCLicenseOfferStateCommitted, //!< The user has committed to (bought) the offer. + OCLicenseOfferStateExpired //!< The user has committed to the offer, but it has expired (f.ex. for subscriptions that have ended). +}; + +@interface OCLicenseOffer : NSObject + ++ (instancetype)offerWithIdentifier:(OCLicenseOfferIdentifier)identifier type:(OCLicenseType)type product:(OCLicenseProductIdentifier)productIdentifier; + +#pragma mark - Metadata +@property(nullable,strong) OCLicenseOfferIdentifier identifier; +@property(weak) OCLicenseProvider *provider; + +#pragma mark - Offer type +@property(assign) OCLicenseType type; + +#pragma mark - Product info +@property(strong) OCLicenseProductIdentifier productIdentifier; +@property(nullable,strong) OCLicenseOfferGroupIdentifier groupIdentifier; + +@property(nullable,strong,nonatomic) OCLicenseProduct *product; + +@property(nullable,strong) NSString *localizedTitle; +@property(nullable,strong) NSString *localizedDescription; + +#pragma mark - State +@property(assign) OCLicenseOfferState state; + +- (OCLicenseOfferState)stateInEnvironment:(OCLicenseEnvironment *)environment; //!< Computes the state based on environment. The OCLicenseOfferStateRedundant and OCLicenseOfferStateExpired states can only occur as a result of this method, not as value for .state. + +#pragma mark - Availability +@property(nullable,strong) NSDate *fromDate; +@property(nullable,strong) NSDate *untilDate; +@property(assign,nonatomic) BOOL available; + +#pragma mark - Price information +@property(nullable,strong) NSDecimalNumber *price; +@property(nullable,strong) NSLocale *priceLocale; + +@property(nonatomic,strong) NSString *localizedPriceTag; + +@property(nonatomic,nullable,strong) OCLicenseDuration *trialDuration; +@property(nonatomic,strong) OCLicenseDuration *subscriptionTermDuration; + +#pragma mark - Request offer / Make purchase +@property(nullable,copy) OCLicenseOfferCommitHandler commitHandler; //!< Used as -commitWithOptions: implementation if provided +- (void)commitWithOptions:(nullable OCLicenseOfferCommitOptions)options errorHandler:(nullable OCLicenseOfferCommitErrorHandler)errorHandler; //!< Commits to purchasing the offer, entering a purchase UI flow + +@end + +extern OCLicenseOfferCommitOption OCLicenseOfferCommitOptionBaseViewController; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.m b/ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.m new file mode 100644 index 000000000..79ea9354b --- /dev/null +++ b/ownCloudAppFramework/Licensing/Offer/OCLicenseOffer.m @@ -0,0 +1,196 @@ +// +// OCLicenseOffer.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseOffer.h" +#import "OCLicenseProduct.h" +#import "OCLicenseProvider.h" +#import "OCLicenseManager.h" + +@implementation OCLicenseOffer + ++ (instancetype)offerWithIdentifier:(OCLicenseOfferIdentifier)identifier type:(OCLicenseType)type product:(OCLicenseProductIdentifier)productIdentifier +{ + OCLicenseOffer *offer = [self new]; + + offer.identifier = identifier; + offer.type = type; + offer.productIdentifier = productIdentifier; + + return (offer); +} + +#pragma mark - Product info +- (OCLicenseProduct *)product +{ + return ([self.provider.manager productWithIdentifier:self.productIdentifier]); +} + +#pragma mark - Availability +- (BOOL)available +{ + if ((_fromDate!=nil) && ([_fromDate timeIntervalSinceNow] > 0)) + { + return (NO); + } + + if ((_untilDate!=nil) && ([_untilDate timeIntervalSinceNow] < 0)) + { + return (NO); + } + + return (_available); +} + +- (OCLicenseOfferState)stateInEnvironment:(OCLicenseEnvironment *)environment +{ + if (!self.available) + { + // Offer is not currently available + return (OCLicenseOfferStateUnavailable); + } + + if (_state == OCLicenseOfferStateUncommitted) + { + if ([self.provider.manager authorizationStatusForProduct:_productIdentifier inEnvironment:environment] == OCLicenseAuthorizationStatusGranted) + { + // Contents of offer already paid for + return (OCLicenseOfferStateRedundant); + } + } + + OCLicenseProduct *product; + + if ((product = [self.provider.manager productWithIdentifier:_productIdentifier]) != nil) + { + BOOL allFeaturesUnlocked = (product.contents.count > 0); + + for (OCLicenseFeatureIdentifier featureIdentifier in product.contents) + { + if ([self.provider.manager authorizationStatusForFeature:featureIdentifier inEnvironment:environment] != OCLicenseAuthorizationStatusGranted) + { + allFeaturesUnlocked = NO; + break; + } + } + + if (allFeaturesUnlocked) + { + // Contents of product unlocked by offer already paid for + return (OCLicenseOfferStateRedundant); + } + } + + if (_state == OCLicenseOfferStateCommitted) + { + if ([self.provider.manager authorizationStatusForProduct:_productIdentifier inEnvironment:environment] == OCLicenseAuthorizationStatusExpired) + { + // Offer was taken, but entitlements granted through it have since expired + return (OCLicenseOfferStateExpired); + } + } + + return (_state); +} + +#pragma mark - Price information +- (NSString *)localizedPriceTag +{ + if (_localizedPriceTag == nil) + { + if (_price == nil) + { + return (OCLocalized(@"Free")); + } + else + { + NSNumberFormatter *numberFormatter = [NSNumberFormatter new]; + + numberFormatter.formatterBehavior = NSNumberFormatterBehavior10_4; + numberFormatter.numberStyle = NSNumberFormatterCurrencyStyle; + + if (_priceLocale != nil) + { + numberFormatter.locale = _priceLocale; + } + + _localizedPriceTag = [numberFormatter stringFromNumber:self.price]; + } + } + + + return (_localizedPriceTag); +} + +#pragma mark - Request offer / Make purchase +- (void)commitWithOptions:(nullable OCLicenseOfferCommitOptions)options errorHandler:(nullable OCLicenseOfferCommitErrorHandler)errorHandler +{ + if (_commitHandler != nil) + { + _commitHandler(self, options, errorHandler); + } +} + +#pragma mark - Description ++ (NSString *)stringForOfferState:(OCLicenseOfferState)offerState +{ + switch (offerState) + { + case OCLicenseOfferStateUncommitted: + return (@"uncommitted"); + break; + + case OCLicenseOfferStateUnavailable: + return (@"unavailable"); + break; + + case OCLicenseOfferStateRedundant: + return (@"redundant"); + break; + + case OCLicenseOfferStateInProgress: + return (@"in progress"); + break; + + case OCLicenseOfferStateCommitted: + return (@"committed"); + break; + + case OCLicenseOfferStateExpired: + return (@"expired"); + break; + } + + return (@"unknown"); +} + +- (NSString *)description +{ + return ([NSString stringWithFormat:@"<%@: %p, type: %@, identifier: %@, state: %@, available: %d, productIdentifier: %@, localizedPriceTag: %@%@%@%@%@%@%@>", NSStringFromClass(self.class), self, [OCLicenseProduct stringForType:self.type], self.identifier, [OCLicenseOffer stringForOfferState:self.state], self.available, self.productIdentifier, self.localizedPriceTag, + ((_fromDate != nil) ? [@", fromDate: " stringByAppendingString:_fromDate.description] : @""), + ((_untilDate != nil) ? [@", untilDate: " stringByAppendingString:_untilDate.description] : @""), + ((_trialDuration != nil) ? [@", trialDuration: " stringByAppendingString:_trialDuration.localizedDescription] : @""), + ((_subscriptionTermDuration != nil) ? [@", subscriptionTermDuration: " stringByAppendingString:_subscriptionTermDuration.localizedDescription] : @""), + ((_localizedTitle != nil) ? [@", localizedTitle: " stringByAppendingString:_localizedTitle] : @""), + ((_localizedDescription != nil) ? [@", localizedDescription: " stringByAppendingString:_localizedDescription] : @"") + ]); +} + +@end + +OCLicenseOfferCommitOption OCLicenseOfferCommitOptionBaseViewController = @"baseViewController"; diff --git a/ownCloudAppFramework/Licensing/Product/OCLicenseProduct.h b/ownCloudAppFramework/Licensing/Product/OCLicenseProduct.h new file mode 100644 index 000000000..f4899801f --- /dev/null +++ b/ownCloudAppFramework/Licensing/Product/OCLicenseProduct.h @@ -0,0 +1,52 @@ +// +// OCLicenseProduct.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" + +@class OCLicenseManager; +@class OCLicenseEntitlement; +@class OCLicenseFeature; + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseProduct : NSObject + +@property(weak,nullable) OCLicenseManager *manager; + +#pragma mark - Metadata +@property(strong,readonly) OCLicenseProductIdentifier identifier; //!< Identifier uniquely identifying the product + +@property(strong) NSString *localizedName; //!< Localized name of the product +@property(nullable,strong) NSString *localizedDescription; //!< Localized description of the product + +#pragma mark - Feature set +@property(nullable,strong,readonly) NSArray *contents; //!< Array of feature identifiers of features contained in this product +@property(nullable,strong,nonatomic) NSArray *features; //!< Array of features contained in this product + +#pragma mark - Access information +@property(nullable,strong,nonatomic) NSArray *entitlements; //!< Array of entitlements relevant to this product + ++ (instancetype)productWithIdentifier:(OCLicenseProductIdentifier)identifier name:(NSString *)localizedName description:(nullable NSString *)localizedDescription contents:(NSArray *)contents; + +#pragma mark - Tools ++ (NSString *)stringForType:(OCLicenseType)type; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Product/OCLicenseProduct.m b/ownCloudAppFramework/Licensing/Product/OCLicenseProduct.m new file mode 100644 index 000000000..614589623 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Product/OCLicenseProduct.m @@ -0,0 +1,82 @@ +// +// OCLicenseProduct.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseProduct.h" +#import "OCLicenseManager+Internal.h" + +@implementation OCLicenseProduct + ++ (instancetype)productWithIdentifier:(OCLicenseProductIdentifier)identifier name:(NSString *)localizedName description:(nullable NSString *)localizedDescription contents:(NSArray *)contents +{ + return ([[self alloc] initWithIdentifier:identifier name:localizedName description:localizedDescription contents:contents]); +} + +- (instancetype)initWithIdentifier:(OCLicenseProductIdentifier)identifier name:(NSString *)localizedName description:(nullable NSString *)localizedDescription contents:(NSArray *)contents +{ + if ((self = [super init]) != nil) + { + _identifier = identifier; + + _localizedName = localizedName; + _localizedDescription = localizedDescription; + + _contents = contents; + } + + return (self); +} + +- (NSArray *)entitlements +{ + @synchronized(self) + { + if (_entitlements == nil) + { + _entitlements = [self.manager _entitlementsForProduct:self]; + } + + return (_entitlements); + } +} + +#pragma mark - Tools ++ (NSString *)stringForType:(OCLicenseType)type +{ + switch (type) + { + case OCLicenseTypeNone: + return (@"none"); + break; + + case OCLicenseTypeTrial: + return (@"trial"); + break; + + case OCLicenseTypePurchase: + return (@"purchase"); + break; + + case OCLicenseTypeSubscription: + return (@"subscription"); + break; + } + + return (@"unknown"); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.h b/ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.h new file mode 100644 index 000000000..4414e795f --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.h @@ -0,0 +1,49 @@ +// +// OCLicenseAppStoreItem.h +// ownCloud +// +// Created by Felix Schwarz on 25.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import +#import "OCLicenseOffer.h" +#import "OCLicenseDuration.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* OCLicenseAppStoreProductIdentifier; + +@interface OCLicenseAppStoreItem : NSObject + +@property(strong,readonly) OCLicenseAppStoreProductIdentifier identifier; + +@property(assign,readonly) OCLicenseType type; + +@property(strong,readonly) OCLicenseDuration *trialDuration; + +@property(strong,readonly) OCLicenseProductIdentifier productIdentifier; + +@property(nullable,strong) SKProduct *storeProduct; +@property(nullable,strong) OCLicenseOffer *offer; + ++ (instancetype)trialWithAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier trialDuration:(OCLicenseDuration *)trialDuration productIdentifier:(OCLicenseProductIdentifier)productIdentifier; + ++ (instancetype)nonConsumableIAPWithAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier productIdentifier:(OCLicenseProductIdentifier)productIdentifier; + ++ (instancetype)subscriptionWithAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier productIdentifier:(OCLicenseProductIdentifier)productIdentifier trialDuration:(OCLicenseDuration *)trialDuration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.m b/ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.m new file mode 100644 index 000000000..449f68e47 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Items/OCLicenseAppStoreItem.m @@ -0,0 +1,52 @@ +// +// OCLicenseAppStoreItem.m +// ownCloud +// +// Created by Felix Schwarz on 25.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseAppStoreItem.h" + +@implementation OCLicenseAppStoreItem + ++ (instancetype)trialWithAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier trialDuration:(OCLicenseDuration *)trialDuration productIdentifier:(OCLicenseProductIdentifier)productIdentifier +{ + return ([[self alloc] initWithType:OCLicenseTypeTrial identifier:identifier productIdentifier:productIdentifier trialDuration:trialDuration]); +} + ++ (instancetype)nonConsumableIAPWithAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier productIdentifier:(OCLicenseProductIdentifier)productIdentifier +{ + return ([[self alloc] initWithType:OCLicenseTypePurchase identifier:identifier productIdentifier:productIdentifier trialDuration:nil]); +} + ++ (instancetype)subscriptionWithAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier productIdentifier:(OCLicenseProductIdentifier)productIdentifier trialDuration:(OCLicenseDuration *)trialDuration +{ + return ([[self alloc] initWithType:OCLicenseTypeSubscription identifier:identifier productIdentifier:productIdentifier trialDuration:trialDuration]); +} + +- (instancetype)initWithType:(OCLicenseType)type identifier:(OCLicenseAppStoreProductIdentifier)identifier productIdentifier:(OCLicenseProductIdentifier)productIdentifier trialDuration:(OCLicenseDuration *)trialDuration +{ + if ((self = [self init]) != nil) + { + _type = type; + _identifier = identifier; + _productIdentifier = productIdentifier; + + _trialDuration = trialDuration; + } + + return (self); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h new file mode 100644 index 000000000..a33543fa7 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.h @@ -0,0 +1,70 @@ +// +// OCLicenseAppStoreProvider.h +// ownCloud +// +// Created by Felix Schwarz on 24.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +#import "OCLicenseProvider.h" +#import "OCLicenseAppStoreItem.h" +#import "OCLicenseAppStoreReceipt.h" +#import "OCLicenseManager.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^OCLicenseAppStoreRestorePurchasesCompletionHandler)(NSError * _Nullable error); +typedef void(^OCLicenseAppStoreRefreshProductsCompletionHandler)(NSError * _Nullable error); + +typedef NS_ENUM(NSUInteger, OCLicenseAppStoreProviderError) +{ + OCLicenseAppStoreProviderErrorPurchasesNotAllowed +}; + +@interface OCLicenseAppStoreProvider : OCLicenseProvider +{ + OCLicenseAppStoreReceipt *_receipt; +} + +@property(nullable,strong,readonly,nonatomic) OCLicenseAppStoreReceipt *receipt; + +@property(strong) NSArray *items; + +@property(nonatomic,readonly) BOOL purchasesAllowed; + +#pragma mark - Init +- (instancetype)initWithItems:(NSArray *)items; + +#pragma mark - Refreshing products +- (void)refreshProductsWithCompletionHandler:(OCLicenseAppStoreRefreshProductsCompletionHandler)completionHandler; //!< Re-requests the list of products from the App Store +- (void)refreshProductsIfNeededWithCompletionHandler:(OCLicenseAppStoreRefreshProductsCompletionHandler)completionHandler; //!< Requests the list of products from the App Store if it hasn't already (or failed due to f.ex. a lack of connectivity) + +#pragma mark - Restoring IAPs +- (void)restorePurchasesWithCompletionHandler:(OCLicenseAppStoreRestorePurchasesCompletionHandler)completionHandler; //!< Restores in-app purchases and calls the completion handler when done + +@end + +@interface OCLicenseManager (AppStore) + +@property(readonly,nonatomic,strong,nullable,class) OCLicenseAppStoreProvider *appStoreProvider; //! Convenience accessor for the AppStore Provider + +@end + +extern OCLicenseProviderIdentifier OCLicenseProviderIdentifierAppStore; + +extern NSErrorDomain OCLicenseAppStoreProviderErrorDomain; + +NS_ASSUME_NONNULL_END + diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m new file mode 100644 index 000000000..9c1d2ec24 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/OCLicenseAppStoreProvider.m @@ -0,0 +1,813 @@ +// +// OCLicenseAppStoreProvider.m +// ownCloud +// +// Created by Felix Schwarz on 24.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import + +#import "OCLicenseAppStoreProvider.h" +#import "OCLicenseManager.h" +#import "OCLicenseAppStoreReceipt.h" +#import "OCLicenseEntitlement.h" +#import "OCLicenseProduct.h" +#import "OCLicenseTransaction.h" +#import "OCLicenseOffer.h" + +#define AppStoreOfferIdentifier(appStoreProductIdentifier) [@"appstore." stringByAppendingString:appStoreProductIdentifier] + +OCIPCNotificationName OCIPCNotificationNameLicenseAppStoreProviderDataChanged = @"org.owncloud.app-store-provider.data-changed"; + +@interface OCLicenseAppStoreProvider () +{ + OCLicenseAppStoreRefreshProductsCompletionHandler _productsRefreshCompletionHandler; + BOOL _setupTransactionObserver; + + NSMutableDictionary *_offerStateByAppStoreProductIdentifier; + + NSMutableDictionary *_commitErrorHandlerByProductIdentifier; + + OCLicenseAppStoreRestorePurchasesCompletionHandler _restorePurchasesCompletionHandler; + BOOL _appStoreReceiptNeedsReload; +} + +@property(nullable,strong) SKProductsRequest *request; +@property(nullable,strong) SKProductsResponse *response; + +@end + +@implementation OCLicenseAppStoreProvider + +#pragma mark - Init +- (instancetype)initWithItems:(NSArray *)items +{ + if ((self = [super initWithIdentifier:OCLicenseProviderIdentifierAppStore]) != nil) + { + _items = items; + _offerStateByAppStoreProductIdentifier = [NSMutableDictionary new]; + + _commitErrorHandlerByProductIdentifier = [NSMutableDictionary new]; + + self.localizedName = OCLocalized(@"App Store"); + + __weak OCLicenseAppStoreProvider *weakSelf = self; + + [OCIPNotificationCenter.sharedNotificationCenter addObserver:self forName:OCIPCNotificationNameLicenseAppStoreProviderDataChanged withHandler:^(OCIPNotificationCenter * _Nonnull notificationCenter, id _Nonnull observer, OCIPCNotificationName _Nonnull notificationName) { + [weakSelf loadReceipt]; + }]; + } + + return (self); +} + +- (void)dealloc +{ + [OCIPNotificationCenter.sharedNotificationCenter removeObserver:self forName:OCIPCNotificationNameLicenseAppStoreProviderDataChanged]; +} + +#pragma mark - Purchases allowed +- (BOOL)purchasesAllowed +{ + return (SKPaymentQueue.canMakePayments); +} + +#pragma mark - Mapping +- (nullable OCLicenseProductIdentifier)productIdentifierForAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier +{ + return ([self itemForAppStoreIdentifier:identifier].productIdentifier); +} + +- (nullable OCLicenseAppStoreItem *)itemForAppStoreIdentifier:(OCLicenseAppStoreProductIdentifier)identifier +{ + if (identifier == nil) + { + return (nil); + } + + for (OCLicenseAppStoreItem *item in _items) + { + if ([item.identifier isEqual:identifier]) + { + return (item); + } + } + + return (nil); +} + +#pragma mark - Transaction access +- (void)retrieveTransactionsWithCompletionHandler:(void (^)(NSError * _Nullable, NSArray * _Nullable))completionHandler +{ + if (self.receipt == nil) + { + [self restorePurchasesWithCompletionHandler:^(NSError * _Nullable error) { + completionHandler(error, [self _transactionsFromReceipt:self.receipt]); + }]; + } + else + { + completionHandler(nil, [self _transactionsFromReceipt:self.receipt]); + } +} + +- (NSArray *)_transactionsFromReceipt:(OCLicenseAppStoreReceipt *)receipt +{ + NSMutableArray *transactions = nil; + + if (receipt != nil) + { + transactions = [NSMutableArray new]; + + if (receipt.originalAppVersion != nil) + { + [transactions addObject:[OCLicenseTransaction transactionWithProvider:self tableRows:@[ + @{ OCLocalized(@"Purchased App Version") : receipt.originalAppVersion }, + @{ OCLocalized(@"Receipt Date") : (receipt.creationDate != nil) ? receipt.creationDate : @"-" } + ]]]; + } + + for (OCLicenseAppStoreReceiptInAppPurchase *iap in receipt.inAppPurchases) + { + OCLicenseProductIdentifier productID = [self productIdentifierForAppStoreIdentifier:iap.productID]; + OCLicenseProduct *product = [self.manager productWithIdentifier:productID]; + OCLicenseAppStoreItem *item = [self itemForAppStoreIdentifier:iap.productID]; + + OCLicenseTransaction *transaction; + + transaction = [OCLicenseTransaction transactionWithProvider:self + identifier:iap.webOrderLineItemID.stringValue + type:item.type + quantity:iap.quantity.integerValue + name:((item.storeProduct.localizedTitle != nil) ? item.storeProduct.localizedTitle : ((product != nil) ? product.localizedName : iap.productID)) + productIdentifier:product.identifier + date:iap.purchaseDate + endDate:iap.subscriptionExpirationDate + cancellationDate:iap.cancellationDate]; + if ((transaction.type == OCLicenseTypeSubscription) && (iap.subscriptionExpirationDate.timeIntervalSinceNow > 0) && ((iap.cancellationDate==nil) || (iap.cancellationDate.timeIntervalSinceNow > 0))) + { + transaction.links = @{ OCLocalized(@"Manage subscription") : [NSURL URLWithString:@"https://apps.apple.com/account/subscriptions"] }; + } + + [transactions addObject:transaction]; + } + } + + return (transactions); +} + +#pragma mark - Start providing +- (void)startProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + if (!SKPaymentQueue.canMakePayments) + { + OCLogWarning(@"SKPaymentQueue: can't make payments"); + completionHandler(self, nil); + return; + } + + __weak OCLicenseAppStoreProvider *weakSelf = self; + + [self refreshProductsWithCompletionHandler:^(NSError * _Nullable error) { + completionHandler(weakSelf, error); + }]; + + if (!_setupTransactionObserver) + { + // Did setup + _setupTransactionObserver = YES; + + // Add the provider as transaction observer + [SKPaymentQueue.defaultQueue addTransactionObserver:self]; // needs to be called in -[UIApplicationDelegate application:didFinishLaunchingWithOptions:] to not miss a transaction + + // Add termination notification observer + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_processWillTerminate) name:UIApplicationWillTerminateNotification object:nil]; + } + + // Load & refresh receipt as needed + [self loadReceipt]; + [self reloadReceiptIfNeeded]; +} + +- (OCLicenseAppStoreReceipt *)receipt +{ + if (_receipt == nil) + { + [self loadReceipt]; + } + + return (_receipt); +} + +- (void)_ipcNotifyToReloadReceipt +{ + [OCIPNotificationCenter.sharedNotificationCenter postNotificationForName:OCIPCNotificationNameLicenseAppStoreProviderDataChanged ignoreSelf:YES]; +} + +- (void)loadReceipt +{ + OCLicenseAppStoreReceipt *receipt = OCLicenseAppStoreReceipt.defaultReceipt; + OCLicenseAppStoreReceiptParseError parseError; + + if ((parseError = [receipt parse]) != OCLicenseAppStoreReceiptParseErrorNone) + { + OCLogError(@"Error %ld parsing App Store receipt.", (long)parseError); + } + + [self willChangeValueForKey:@"receipt"]; + _receipt = receipt; + [self didChangeValueForKey:@"receipt"]; + + OCLogDebug(@"App Store Receipt loaded: %@", _receipt); + + [self recomputeEntitlements]; + [self recomputeOffers]; +} + +- (void)setReceiptNeedsReload +{ + @synchronized(self) + { + _appStoreReceiptNeedsReload = YES; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self reloadReceiptIfNeeded]; + }); +} + +- (void)reloadReceiptIfNeeded +{ + OCLicenseAppStoreReceipt *receipt = self.receipt; + BOOL needsReload = NO; + + if (receipt != nil) + { + if ((receipt.expirationDate != nil) && (receipt.expirationDate.timeIntervalSinceNow < 0)) + { + // Receipt expired + needsReload = YES; + } + else + { + for (OCLicenseAppStoreReceiptInAppPurchase *iap in receipt.inAppPurchases) + { + if ((iap.subscriptionExpirationDate != nil) && (iap.subscriptionExpirationDate.timeIntervalSinceNow < 0) && // IAP has expired + ([iap.subscriptionExpirationDate timeIntervalSinceDate:receipt.creationDate] > 0)) // The receipt was created before the IAP has expired + { + // Subscription expired + needsReload = YES; + break; + } + } + } + } + + BOOL reloadRequested = NO; + + @synchronized(self) + { + reloadRequested = _appStoreReceiptNeedsReload; + _appStoreReceiptNeedsReload = NO; + } + + if (needsReload || reloadRequested) + { + OCLogDebug(@"App Store Receipt needs reload"); + + [self restorePurchasesWithCompletionHandler:^(NSError * _Nullable error) { + OCLogDebug(@"Restored purchases with error=%@", error); + + if (error != nil) + { + OCLogError(@"Error restoring purchases: %@", error); + return; + } + + [self loadReceipt]; + }]; + } +} + +- (void)_processWillTerminate +{ + // Remove the provider on app termination + [self.manager removeProvider:self]; +} + +- (void)stopProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + // Cancel product request if still ongoing + if (_request != nil) + { + [_request cancel]; + _request = nil; + } + + if (_setupTransactionObserver) + { + // Remove provider as transaction observer + [SKPaymentQueue.defaultQueue removeTransactionObserver:self]; + + // Remove termination notification observer + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillTerminateNotification object:nil]; + } +} + +#pragma mark - Entitlements & Offers +- (void)recomputeEntitlements +{ + OCLicenseAppStoreReceipt *receipt = _receipt; + NSMutableArray *entitlements = [NSMutableArray new]; + + for (OCLicenseAppStoreItem *item in self.items) + { + // Find corresponding IAPs + NSMutableArray *itemIAPs = nil; + + for (OCLicenseAppStoreReceiptInAppPurchase *iap in receipt.inAppPurchases) + { + if ([iap.productID isEqualToString:item.identifier]) + { + if (itemIAPs == nil) + { + itemIAPs = [NSMutableArray new]; + } + + [itemIAPs addObject:iap]; + } + } + + if (itemIAPs != nil) + { + // IAPs found => create entitlements + OCLicenseEntitlement *entitlement; + OCLicenseAppStoreReceiptInAppPurchase *iap = itemIAPs.lastObject; // If there's more than one IAP, assume the last to be the relevant one + NSDate *expiryDate = nil; + NSDate *purchaseDate = (iap.originalPurchaseDate != nil) ? iap.originalPurchaseDate : iap.purchaseDate; + + switch (item.type) + { + case OCLicenseTypeNone: + break; + + case OCLicenseTypeTrial: + expiryDate = [item.trialDuration dateWithDurationAddedTo:purchaseDate]; + break; + + case OCLicenseTypeSubscription: + expiryDate = iap.subscriptionExpirationDate; + break; + + case OCLicenseTypePurchase: + break; + } + + entitlement = [OCLicenseEntitlement entitlementWithIdentifier:nil forProduct:item.productIdentifier type:item.type valid:YES expiryDate:expiryDate applicability:nil]; + + [entitlements addObject:entitlement]; + } + } + + self.entitlements = (entitlements.count > 0) ? entitlements : nil; +} + +- (void)recomputeOffers +{ + // Create offers from items + NSMutableArray *offers = [NSMutableArray new]; + + for (OCLicenseAppStoreItem *item in _items) + { + SKProduct *storeProduct = nil; + + if ((storeProduct = item.storeProduct) != nil) + { + OCLicenseOffer *offer = nil; + OCLicenseAppStoreProductIdentifier appStoreProductIdentifier = storeProduct.productIdentifier; + + offer = [OCLicenseOffer offerWithIdentifier:AppStoreOfferIdentifier(appStoreProductIdentifier) type:item.type product:item.productIdentifier]; + + offer.price = storeProduct.price; + offer.priceLocale = storeProduct.priceLocale; + + offer.available = YES; + + offer.localizedTitle = storeProduct.localizedTitle; + offer.localizedDescription = storeProduct.localizedDescription; + + NSMutableDictionary *commitErrorHandlerByProductIdentifier = _commitErrorHandlerByProductIdentifier; + + offer.commitHandler = ^(OCLicenseOffer * _Nonnull offer, OCLicenseOfferCommitOptions _Nullable options, OCLicenseOfferCommitErrorHandler _Nullable errorHandler) { + OCLicenseAppStoreProvider *appStoreProvider = [offer.provider isKindOfClass:[OCLicenseAppStoreProvider class]] ? (OCLicenseAppStoreProvider *)offer.provider : nil; + + if (appStoreProvider != nil) + { + if (!appStoreProvider.purchasesAllowed) + { + // Present alert + if (errorHandler != nil) + { + errorHandler([NSError errorWithDomain:OCLicenseAppStoreProviderErrorDomain code:OCLicenseAppStoreProviderErrorPurchasesNotAllowed userInfo:@{ + NSLocalizedDescriptionKey : OCLocalized(@"Purchases are not allowed on this device.") + }]); + } + } + else + { + if ((errorHandler != nil) && (appStoreProductIdentifier!=nil)) + { + commitErrorHandlerByProductIdentifier[appStoreProductIdentifier] = [errorHandler copy]; + } + + [appStoreProvider requestPaymentFor:storeProduct]; + } + } + }; + + // Subscription and trial properties + offer.trialDuration = item.trialDuration; + + if (storeProduct.subscriptionPeriod != nil) + { + offer.subscriptionTermDuration = storeProduct.subscriptionPeriod.licenseDuration; + } + + if (@available(iOS 12, *)) + { + offer.groupIdentifier = storeProduct.subscriptionGroupIdentifier; + } + + // Compute state + [self _updateStateForOffer:offer withAppStoreProductIdentifier:appStoreProductIdentifier]; + + // Add offer + if (offer != nil) + { + [offers addObject:offer]; + } + } + } + + self.offers = (offers.count > 0) ? offers : nil; +} + +- (void)_updateStateForOffer:(OCLicenseOffer *)offer withAppStoreProductIdentifier:(OCLicenseAppStoreProductIdentifier)appStoreProductIdentifier +{ + OCLicenseAppStoreReceipt *receipt = _receipt; + + // Derive state from payment queue + if (_offerStateByAppStoreProductIdentifier[appStoreProductIdentifier] != nil) + { + offer.state = _offerStateByAppStoreProductIdentifier[appStoreProductIdentifier].unsignedIntegerValue; + } + + // Derive state from receipt + for (OCLicenseAppStoreReceiptInAppPurchase *iap in receipt.inAppPurchases) + { + if ([iap.productID isEqual:appStoreProductIdentifier] && ( + (iap.subscriptionExpirationDate == nil) || // not a subscription + ((iap.subscriptionExpirationDate != nil) && (iap.subscriptionExpirationDate.timeIntervalSinceNow > 0)) // subscription valid + )) + { + offer.state = OCLicenseOfferStateCommitted; + } + } +} + +#pragma mark - Product request delegate +- (void)refreshProductsWithCompletionHandler:(OCLicenseAppStoreRefreshProductsCompletionHandler)completionHandler +{ + NSMutableSet *appStoreIdentifiers = [NSMutableSet new]; + + // Build product request + for (OCLicenseAppStoreItem *item in _items) + { + [appStoreIdentifiers addObject:item.identifier]; + } + + // Store completion handler and start request if needed + @synchronized(self) + { + OCLicenseAppStoreRefreshProductsCompletionHandler existingCompletionHandler = _productsRefreshCompletionHandler; + + if (existingCompletionHandler != nil) + { + _productsRefreshCompletionHandler = ^(NSError *error) { + existingCompletionHandler(error); + completionHandler(error); + }; + + return; + } + else + { + _productsRefreshCompletionHandler = [completionHandler copy]; + } + + _request = [[SKProductsRequest alloc] initWithProductIdentifiers:appStoreIdentifiers]; + _request.delegate = self; + + [_request start]; + } +} + +- (void)refreshProductsIfNeededWithCompletionHandler:(OCLicenseAppStoreRefreshProductsCompletionHandler)completionHandler +{ + if ((self.offers.count == 0) && self.purchasesAllowed) + { + [self refreshProductsWithCompletionHandler:completionHandler]; + } + else + { + completionHandler(nil); + } +} + +- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response +{ + // Called on success + self.response = response; + + // Parse response + for (SKProduct *product in response.products) + { + for (OCLicenseAppStoreItem *item in _items) + { + if ([item.identifier isEqual:product.productIdentifier]) + { + item.storeProduct = product; + break; + } + } + } + + [self recomputeOffers]; +} + +- (void)requestDidFinish:(SKRequest *)request +{ + // Called last on success (not called on error) + + @synchronized(self) + { + OCLogDebug(@"App Store request %@ finished", request); + + if (_productsRefreshCompletionHandler != nil) + { + _productsRefreshCompletionHandler(nil); + _productsRefreshCompletionHandler = nil; + } + + if (request == _request) + { + _request = nil; + } + } +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error +{ + // Called last on error + @synchronized(self) + { + OCLogWarning(@"App Store request %@ failed with error %@", request, error); + + if (_productsRefreshCompletionHandler != nil) + { + _productsRefreshCompletionHandler(error); + _productsRefreshCompletionHandler = nil; + } + + if (request == _request) + { + _request = nil; + } + } +} + +#pragma mark - Payment +- (void)requestPaymentFor:(SKProduct *)product +{ + SKPayment *payment; + + OCLogDebug(@"Requesting payment for %@ (productIdentifier=%@)", product, product.productIdentifier); + + if ((product != nil) && ((payment = [SKPayment paymentWithProduct:product]) != nil)) + { + [SKPaymentQueue.defaultQueue addPayment:payment]; + } +} + +#pragma mark - Payment transaction observation +// Sent when the transaction array has changed (additions or state changes). Client should check state of transactions and finish as appropriate. +- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions +{ + OCLogDebug(@"Payment queue updated with transactions %@", transactions); + + for (SKPaymentTransaction *originalTransaction in transactions) + { + OCLicenseOfferState offerState = OCLicenseOfferStateUncommitted; + SKPaymentTransaction *transaction = originalTransaction; + NSError *error = nil; + BOOL finishTransaction = NO; + + // Restored transaction => use the original transaction if available + if ((transaction.transactionState == SKPaymentTransactionStateRestored) && (transaction.originalTransaction != nil)) + { + // Transaction has been restored + transaction = transaction.originalTransaction; + finishTransaction = YES; + } + + // Fall back to any existing offer state + OCLicenseAppStoreProductIdentifier appStoreProductIdentifier = transaction.payment.productIdentifier; + + if (appStoreProductIdentifier != nil) + { + @synchronized(self) + { + if (_offerStateByAppStoreProductIdentifier[appStoreProductIdentifier] != nil) + { + offerState = _offerStateByAppStoreProductIdentifier[appStoreProductIdentifier].unsignedIntegerValue; + } + } + } + + // Translate transaction to offer state + switch (transaction.transactionState) + { + case SKPaymentTransactionStatePurchasing: + offerState = OCLicenseOfferStateInProgress; + // Calling finishTransaction here throws an exception (via documentation) + break; + + case SKPaymentTransactionStateDeferred: + offerState = OCLicenseOfferStateInProgress; + break; + + case SKPaymentTransactionStatePurchased: + offerState = OCLicenseOfferStateCommitted; + finishTransaction = YES; + break; + + case SKPaymentTransactionStateFailed: + OCLogError(@"Transaction failed with error=%@", transaction.error); + + error = transaction.error; + + offerState = OCLicenseOfferStateUncommitted; + finishTransaction = YES; + break; + + case SKPaymentTransactionStateRestored: + OCLogWarning(@"Restored App Store transaction without original? %@ %@", transaction, transaction.originalTransaction); + break; + } + + // Store new offer state + if (appStoreProductIdentifier != nil) + { + @synchronized(self) + { + _offerStateByAppStoreProductIdentifier[appStoreProductIdentifier] = @(offerState); + + // Update offer + OCLicenseOfferIdentifier offerID = AppStoreOfferIdentifier(appStoreProductIdentifier); + + for (OCLicenseOffer *offer in self.offers) + { + if ([offer.identifier isEqual:offerID]) + { + [self _updateStateForOffer:offer withAppStoreProductIdentifier:appStoreProductIdentifier]; + break; + } + } + } + } + + // Finish transaction and remove it from the SKPaymentQueue + if (finishTransaction) + { + [SKPaymentQueue.defaultQueue finishTransaction:originalTransaction]; + } + + // Reload receipt + if ((appStoreProductIdentifier != nil) && finishTransaction && (offerState == OCLicenseOfferStateCommitted)) + { + [self loadReceipt]; + [self _ipcNotifyToReloadReceipt]; + } + + // Report errors + if ((appStoreProductIdentifier != nil) && finishTransaction) + { + if (_commitErrorHandlerByProductIdentifier[appStoreProductIdentifier] != nil) + { + ((OCLicenseOfferCommitErrorHandler)_commitErrorHandlerByProductIdentifier[appStoreProductIdentifier])(error); + + [_commitErrorHandlerByProductIdentifier removeObjectForKey:appStoreProductIdentifier]; + } + } + } + + [self recomputeEntitlements]; +} + +#pragma mark - Restore IAPs +- (void)restorePurchasesWithCompletionHandler:(OCLicenseAppStoreRestorePurchasesCompletionHandler)completionHandler +{ + OCLogDebug(@"Restoring purchases"); + + @synchronized(self) + { + OCLicenseAppStoreRestorePurchasesCompletionHandler existingCompletionHandler = nil; + + if ((existingCompletionHandler = _restorePurchasesCompletionHandler) != nil) + { + completionHandler = [completionHandler copy]; + + _restorePurchasesCompletionHandler = [^(NSError *error) { + existingCompletionHandler(error); + completionHandler(error); + } copy]; + } + else + { + _restorePurchasesCompletionHandler = [completionHandler copy]; + } + + [SKPaymentQueue.defaultQueue restoreCompletedTransactions]; + } +} + +- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error +{ + OCLogWarning(@"Payment queue restore failed with error %@", error); + + @synchronized(self) + { + [self loadReceipt]; + [self _ipcNotifyToReloadReceipt]; + + if (_restorePurchasesCompletionHandler != nil) + { + _restorePurchasesCompletionHandler(error); + _restorePurchasesCompletionHandler = nil; + } + } +} + +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue +{ + OCLogDebug(@"Payment queue restore completed successfully"); + + @synchronized(self) + { + [self loadReceipt]; + [self _ipcNotifyToReloadReceipt]; + + if (_restorePurchasesCompletionHandler != nil) + { + _restorePurchasesCompletionHandler(nil); + _restorePurchasesCompletionHandler = nil; + } + } +} + ++ (NSArray *)logTags +{ + return (@[@"Licensing", @"AppStore"]); +} + +- (NSArray *)logTags +{ + return (@[@"Licensing", @"AppStore"]); +} + +@end + +@implementation OCLicenseManager (AppStore) + ++ (OCLicenseAppStoreProvider *)appStoreProvider +{ + return ((OCLicenseAppStoreProvider *)[self.sharedLicenseManager providerForIdentifier:OCLicenseProviderIdentifierAppStore]); +} + +@end + +OCLicenseProviderIdentifier OCLicenseProviderIdentifierAppStore = @"app-store"; + +NSErrorDomain OCLicenseAppStoreProviderErrorDomain = @"OCLicenseAppStoreProviderError"; diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/AppleIncRootCertificate.cer b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/AppleIncRootCertificate.cer new file mode 100644 index 0000000000000000000000000000000000000000..8a9ff247419dd22a07c837ba7394aeabdd0f14e2 GIT binary patch literal 1215 zcmXqLV%crb#JqR`GZP~d5E<~YacZ@Bw0-AgWMpM!Fi0}wHsEAq4rO5zW(o~96gCh9 zakzxJ9199^QWZS&lJyML3{*gZ+`_UDLFd$>lFYQsBg2!4D>>yS-j;I@c+L7YuChh5U7`KHvAiEsOqE5wMI&W5Px=0B&b;#hyADPKr1x`dQTTp(jgCTo z!8UtFgP!fq=lSQ_e%AKXkUH`2+}53ZH{)ckownU-we|}?AHyW>jf!G=C0A{DZzqYZ zUR*fIJvj8>dVR;uKYl+hIQwj|k87R0Pjj_t->jT;Rj-bAq&^<-@B zm%W!-{69S|b&uzbviZg$sSC@eoYZAvW@KPo+{9P~43RPeK43h`@-s62XJG-R8#V)e z5MLO?XEk63QUIB4HrbfL%co zBPgB8DzG#$asX{)0b&Md!c0zKWi)8~WT3^yq0I(NqwGwKVsaTJB?ZM+`ugSN<$8&r zl&P1TpQ{gMB`4||G#-X4W-@5pCe^q(C^aWDF)uk)0hmHdGBS%5lHrLqRUxTTAu+E~ zp&+rS1js5bF3n9XR!B@vPAw>b=t%?WNd@6N1&|%Uq@D!K48=g%l*FPGg_6{wT%d-$ z6ouscyp&8(HYirePg5u@PSruNs30Gx7i1YwCER{crYR^&OfJa;IuB@ONosCtUP-YY za{2^jO7!e*{cX?eJDxY@8r; zW8atJ+3zl;@Sm>qH@UIM?q|jS>=W#7YAu_)gB31Y9ND;kmOoeaf9*e!%UL;V#2vx} zUUwk!R<>!CBEM2a=7;zgw~E zguTASugG_6SFxo3)|+Pa2irq$E}yy6$m#cutA+FG76xsX-aFYzMMzw9>OIdRD+ Xyc@&=R&`yy_2kb5PImJRrKO4hMS!&; literal 0 HcmV?d00001 diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.h b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.h new file mode 100644 index 000000000..e5e05a963 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.h @@ -0,0 +1,29 @@ +// +// NSDate+RFC3339.h +// ownCloudApp +// +// Created by Felix Schwarz on 03.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSDate (RFC3339) + ++ (nullable NSDate *)dateFromRFC3339DateString:(NSString *)dateString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.m b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.m new file mode 100644 index 000000000..8d21a32d6 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/NSDate+RFC3339.m @@ -0,0 +1,65 @@ +// +// NSDate+RFC3339.m +// ownCloudApp +// +// Created by Felix Schwarz on 03.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "NSDate+RFC3339.h" + +@implementation NSDate (RFC3339) + ++ (NSDate *)dateFromRFC3339DateString:(NSString *)dateString +{ + static dispatch_once_t onceToken = 0; + static NSDateFormatter *rfc3339DateFormatter; + NSDate *parsedDate = nil; + + dispatch_once(&onceToken, ^{ + NSLocale *posixLocale; + + if ((posixLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]) != nil) + { + rfc3339DateFormatter = [[NSDateFormatter alloc] init]; + rfc3339DateFormatter.locale = posixLocale; + rfc3339DateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + rfc3339DateFormatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZ"; + } + }); + + if (rfc3339DateFormatter != nil) + { + NSRange timezoneDelimiterRange; + + timezoneDelimiterRange = [dateString rangeOfString:@"+"]; + if (timezoneDelimiterRange.location == NSNotFound) + { + if ([dateString length] > 10) + { + timezoneDelimiterRange = [dateString rangeOfString:@"-" options:0 range:NSMakeRange(10, [dateString length]-10)]; + } + } + + if (timezoneDelimiterRange.location != NSNotFound) + { + dateString = [dateString stringByReplacingOccurrencesOfString:@":" withString:@"" options:0 range:NSMakeRange(timezoneDelimiterRange.location, [dateString length]-timezoneDelimiterRange.location)]; + } + + parsedDate = [rfc3339DateFormatter dateFromString:dateString]; + } + + return (parsedDate); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.h b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.h new file mode 100644 index 000000000..62a6333b1 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.h @@ -0,0 +1,41 @@ +// +// OCASN1.h +// ownCloudApp +// +// Created by Felix Schwarz on 03.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseAppStoreReceipt.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCASN1 : NSObject + +@property(nullable,assign) void *data; +@property(assign) NSUInteger length; + +@property(strong,nonatomic,readonly,nullable) NSString *UTF8String; +@property(strong,nonatomic,readonly,nullable) NSDate *RFC3339Date; +@property(strong,nonatomic,readonly,nullable) NSNumber *integer; + +@property(strong,nullable) NSMutableArray *containers; + +- (instancetype)initWithData:(void *)data length:(size_t)length; + +- (OCLicenseAppStoreReceiptParseError)parseSetsOfSequencesWithContainerProvider:(nullable id(^)(void))containerProvider interpreter:(OCLicenseAppStoreReceiptParseError(^)(id container, OCLicenseAppStoreReceiptFieldType, OCASN1 *contents))interpreter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.m b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.m new file mode 100644 index 000000000..ec8a2c861 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Parser Support/OCASN1.m @@ -0,0 +1,219 @@ +// +// OCASN1.m +// ownCloudApp +// +// Created by Felix Schwarz on 03.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCASN1.h" +#import "NSDate+RFC3339.h" + +#import + +#import +#import +#import +#import +#import + +@implementation OCASN1 + +- (instancetype)initWithData:(void *)data length:(size_t)length +{ + if ((self = [super init]) != nil) + { + _data = data; + _length = length; + } + + return (self); +} + +- (NSDate *)RFC3339Date +{ + NSString *rfcDateString; + + if ((rfcDateString = self.IA5String) != nil) + { + return ([NSDate dateFromRFC3339DateString:rfcDateString]); + } + + return (nil); +} + +- (NSString *)_stringWithTag:(int)tag encoding:(NSStringEncoding)encoding +{ + int contentClass=0, contentTag=0; + long contentLength=0; + const unsigned char *p_content = NULL; + + contentTag = 0; + contentLength = 0; + p_content = _data; + + ASN1_get_object(&p_content, &contentLength, &contentTag, &contentClass, _length); + if (contentTag!=tag) { return(nil); } + + return ([[NSString alloc] initWithBytes:(const void *)p_content length:contentLength encoding:encoding]); +} + +- (NSString *)UTF8String +{ + return ([self _stringWithTag:V_ASN1_UTF8STRING encoding:NSUTF8StringEncoding]); +} + +- (NSString *)IA5String +{ + return ([self _stringWithTag:V_ASN1_IA5STRING encoding:NSASCIIStringEncoding]); +} + +- (NSNumber *)integer +{ + int contentClass=0, contentTag=0; + long contentLength=0; + const unsigned char *p_content = NULL; + + contentTag = 0; + contentLength = 0; + p_content = _data; + + ASN1_get_object(&p_content, &contentLength, &contentTag, &contentClass, _length); + if (contentTag == V_ASN1_INTEGER) + { + NSUInteger number = 0; + + for (NSUInteger offset=0; offset < contentLength; offset++) + { + number = (number << 8L) | p_content[offset]; + } + + return (@(number)); + } + + return (nil); +} + +- (OCLicenseAppStoreReceiptParseError)parseSetsOfSequencesWithContainerProvider:(nullable id(^)(void))containerProvider interpreter:(OCLicenseAppStoreReceiptParseError(^)(id container, OCLicenseAppStoreReceiptFieldType, OCASN1 *contents))interpreter +{ + OCLicenseAppStoreReceiptParseError error = OCLicenseAppStoreReceiptParseErrorNone; + + const unsigned char *p_octetData = _data; + const unsigned char *p_octetEndByte = p_octetData + _length; + long objLength; + int objTag, objClass; + + // Parse sets + while(p_octetData < p_octetEndByte) + { + const unsigned char *p_setEnd; + + // Get set size + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_octetEndByte-p_octetData); + if (objTag != V_ASN1_SET) { break; } + + p_setEnd = p_octetData + objLength; + + id parseResultContainer = (containerProvider != nil) ? containerProvider() : nil; + + // Parse set + while (p_octetData < p_setEnd) + { + const unsigned char *p_seqEnd; + int itemAttribType=0, itemAttribVersion=0; + + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_setEnd-p_octetData); + if (objTag != V_ASN1_SEQUENCE) { break; } + + p_seqEnd = p_octetData + objLength; + + // Parse seq + // Get attribute type + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_seqEnd-p_octetData); + if (objTag == V_ASN1_INTEGER) + { + if (objLength == 1) + { + #ifndef __clang_analyzer__ + itemAttribType = p_octetData[0]; + #endif + } + else + { + if (objLength == 2) + { + #ifndef __clang_analyzer__ + itemAttribType = (p_octetData[0] << 8)|p_octetData[1]; + #endif + } + else + { + break; + } + } + } + p_octetData += objLength; + + // Get attribute version + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_seqEnd-p_octetData); + if ((objTag != V_ASN1_INTEGER) || (objLength!=1)) { break; } + #ifndef __clang_analyzer__ + itemAttribVersion = p_octetData[0]; + #endif /* __clang_analyzer__ */ + p_octetData += objLength; + + // Get value + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_seqEnd-p_octetData); + if (objTag == V_ASN1_OCTET_STRING) + { + // Interpret value + OCLicenseAppStoreReceiptParseError interpreterError; + + if ((interpreterError = interpreter(parseResultContainer, itemAttribType, [[OCASN1 alloc] initWithData:(void *)p_octetData length:objLength])) != OCLicenseAppStoreReceiptParseErrorNone) + { + error = interpreterError; + break; + } + } + p_octetData += objLength; + + // Ignore all other objects until end of sequence + while (p_octetData < p_seqEnd) + { + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_seqEnd-p_octetData); + p_octetData += objLength; + }; + }; + + // Ignore all other objects until end of set + while (p_octetData < p_setEnd) + { + ASN1_get_object(&p_octetData, &objLength, &objTag, &objClass, p_setEnd-p_octetData); + p_octetData += objLength; + } + + if (parseResultContainer != nil) + { + if (_containers == nil) + { + _containers = [NSMutableArray new]; + } + + [_containers addObject:parseResultContainer]; + } + }; + + return (error); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.h b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.h new file mode 100644 index 000000000..4fbf33e29 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.h @@ -0,0 +1,107 @@ +// +// OCLicenseAppStoreReceipt.h +// ownCloud +// +// Created by Felix Schwarz on 28.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseAppStoreItem.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, OCLicenseAppStoreReceiptParseError) +{ + OCLicenseAppStoreReceiptParseErrorNone, + + OCLicenseAppStoreReceiptParseErrorNoReceipt, + OCLicenseAppStoreReceiptParseErrorNoRootCA, + OCLicenseAppStoreReceiptParseErrorNoDeviceID, + + OCLicenseAppStoreReceiptParseErrorPKCS7Decode, + OCLicenseAppStoreReceiptParseErrorPKCS7Unsigned, + OCLicenseAppStoreReceiptParseErrorPKCS7ContentsNotData, + + OCLicenseAppStoreReceiptParseErrorX509Store, + OCLicenseAppStoreReceiptParseErrorX509Certificate, + + OCLicenseAppStoreReceiptParseErrorSignatureVerification, + + OCLicenseAppStoreReceiptParseErrorASN1NotASet, + OCLicenseAppStoreReceiptParseErrorASN1UnexpectedType +}; + +typedef NS_ENUM(NSInteger, OCLicenseAppStoreReceiptFieldType) +{ + // Reference: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1 + + // App Receipt Fields + OCLicenseAppStoreReceiptFieldTypeAppBundleIdentifier = 2, // "bundle_id" + OCLicenseAppStoreReceiptFieldTypeAppVersion = 3, // "application_version" + OCLicenseAppStoreReceiptFieldTypeOpaqueValue = 4, // An opaque value used, with other data, to compute the SHA-1 hash during validation. + OCLicenseAppStoreReceiptFieldTypeSHA1Hash = 5, // A SHA-1 hash, used to validate the receipt. + OCLicenseAppStoreReceiptFieldTypeReceiptCreationDate = 12, // "receipt_creation_date" + OCLicenseAppStoreReceiptFieldTypeInAppPurchase = 17, // "in_app" / SET of in-app purchase receipt attributes + OCLicenseAppStoreReceiptFieldTypeAppOriginalVersion = 19, // "original_application_version" + OCLicenseAppStoreReceiptFieldTypeReceiptExpirationDate = 21, // "expiration_date" + + // In-App Purchase Receipt Fields + OCLicenseAppStoreReceiptFieldTypeIAPQuantity = 1701, + OCLicenseAppStoreReceiptFieldTypeIAPProductID = 1702, + OCLicenseAppStoreReceiptFieldTypeIAPTransactionID = 1703, + OCLicenseAppStoreReceiptFieldTypeIAPPurchaseDate = 1704, + OCLicenseAppStoreReceiptFieldTypeIAPOriginalTransactionID = 1705, + OCLicenseAppStoreReceiptFieldTypeIAPOriginalPurchaseDate = 1706, + OCLicenseAppStoreReceiptFieldTypeIAPSubscriptionExpirationDate = 1708, + OCLicenseAppStoreReceiptFieldTypeIAPWebOrderLineItemID = 1711, + OCLicenseAppStoreReceiptFieldTypeIAPCancellationDate = 1712, + OCLicenseAppStoreReceiptFieldTypeIAPSubscriptionInIntroOfferPeriod = 1719 +}; + +typedef NSString* OCLicenseAppStoreTransactionID; +typedef NSNumber* OCLicenseAppStoreLineItemID; + +@class OCLicenseAppStoreReceiptInAppPurchase; + +@interface OCLicenseAppStoreReceipt : NSObject + +#pragma mark - Certificate and device identity data +@property(strong,nonatomic,readonly,class,nullable) NSData *appleRootCACertificateData; +@property(strong,nonatomic,readonly,class,nullable) NSData *deviceIdentifierData; + +#pragma mark - Receipt data +@property(strong,readonly) NSData *receiptData; + +#pragma mark - Parsed receipt +@property(nullable,strong,readonly) NSDate *creationDate; //!< Date the receipt was created. +@property(nullable,strong,readonly) NSDate *expirationDate; //!< Date the receipt expires. + +@property(nullable,strong,readonly) NSString *appBundleIdentifier; //!< The app's bundle ID. Corresponds to value of Info.plist CFBundleIdentifier. +@property(nullable,strong,readonly) NSString *appVersion; //!< The app's version number. Corresponds to value of Info.plist CFBundleVersion (iOS). +@property(nullable,strong,readonly) NSString *originalAppVersion; //!< The version of the app that was originally purchase. Corresponds to value of Info.plist CFBundleVersion (iOS). + +@property(nullable,strong,readonly) NSArray *inAppPurchases; + + +@property(nullable,readonly,strong,class) OCLicenseAppStoreReceipt *defaultReceipt; + +- (instancetype)initWithReceiptData:(NSData *)receiptData; + +- (OCLicenseAppStoreReceiptParseError)parse; + +@end + +NS_ASSUME_NONNULL_END + +#import "OCLicenseAppStoreReceiptInAppPurchase.h" diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.m b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.m new file mode 100644 index 000000000..5b521231d --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceipt.m @@ -0,0 +1,240 @@ +// +// OCLicenseAppStoreReceipt.m +// ownCloud +// +// Created by Felix Schwarz on 28.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import + +#import +#import +#import +#import +#import + +#import "OCLicenseAppStoreReceipt.h" +#import "OCLicenseAppStoreReceiptInAppPurchase.h" +#import "OCASN1.h" + +#pragma mark - Receipt parser +@implementation OCLicenseAppStoreReceipt + ++ (NSData *)appleRootCACertificateData +{ + NSURL *url; + + if ((url = [[NSBundle bundleForClass:[self class]] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"]) != nil) + { + return ([[NSData alloc] initWithContentsOfURL:url]); + } + + return (nil); +} + ++ (NSData *)deviceIdentifierData +{ + NSUUID *deviceUUID; + NSData *deviceUUIDData = nil; + + if ((deviceUUID = [[UIDevice currentDevice] identifierForVendor]) != nil) + { + uuid_t uuid; + + [deviceUUID getUUIDBytes:(uint8_t *)&uuid]; + + deviceUUIDData = [NSData dataWithBytes:(void *)&uuid length:sizeof(uuid)]; + } + + return (deviceUUIDData); +} + ++ (OCLicenseAppStoreReceipt *)defaultReceipt +{ + if (NSBundle.mainBundle.appStoreReceiptURL != nil) + { + NSData *receiptData; + + if ((receiptData = [NSData dataWithContentsOfURL:NSBundle.mainBundle.appStoreReceiptURL]) != nil) + { + return ([[self alloc] initWithReceiptData:receiptData]); + } + } + + return (nil); +} + +- (instancetype)initWithReceiptData:(NSData *)receiptData +{ + if ((self = [super init]) != nil) + { + _receiptData = receiptData; + } + + return (self); +} + +- (OCLicenseAppStoreReceiptParseError)parse +{ + NSData *receiptData, *rootCAData, *uuidData; + OCLicenseAppStoreReceiptParseError error = OCLicenseAppStoreReceiptParseErrorNone; + + // Fetch essentials + if ((receiptData = _receiptData) == nil) + { + return(OCLicenseAppStoreReceiptParseErrorNoReceipt); + } + + if ((rootCAData = [OCLicenseAppStoreReceipt appleRootCACertificateData]) == nil) + { + return(OCLicenseAppStoreReceiptParseErrorNoRootCA); + } + + if ((uuidData = [OCLicenseAppStoreReceipt deviceIdentifierData]) == nil) + { + return(OCLicenseAppStoreReceiptParseErrorNoDeviceID); + } + + // OpenSSL setup + ERR_load_X509_strings(); + ERR_load_PKCS7_strings(); + OpenSSL_add_all_digests(); + + // Parse receipt + const uint8_t *p_receiptData = receiptData.bytes, *p_caData = rootCAData.bytes; + + PKCS7 *pkcs7 = NULL; + X509 *x509 = NULL; + X509_STORE *x509Store = NULL; + BIO *receiptContents = NULL; + + do + { + // Receipt -> PKCS7 + error = OCLicenseAppStoreReceiptParseErrorPKCS7Decode; + if ((pkcs7 = d2i_PKCS7(NULL, &p_receiptData, receiptData.length)) == NULL) { break; } + + error = OCLicenseAppStoreReceiptParseErrorPKCS7Unsigned; + if (!PKCS7_type_is_signed(pkcs7)) { break; } + + error = OCLicenseAppStoreReceiptParseErrorPKCS7ContentsNotData; + if (!PKCS7_type_is_data(pkcs7->d.sign->contents)) { break; } + + // Root Cert -> X509 + error = OCLicenseAppStoreReceiptParseErrorX509Store; + if ((x509Store = X509_STORE_new()) == NULL) { break; } + + error = OCLicenseAppStoreReceiptParseErrorX509Certificate; + if ((x509 = d2i_X509(NULL, &p_caData, rootCAData.length)) == NULL) { break; } + + X509_STORE_add_cert(x509Store, x509); + + // Verify signature + error = OCLicenseAppStoreReceiptParseErrorSignatureVerification; + if ((receiptContents = BIO_new(BIO_s_mem())) == NULL) { break; } + + int verificationResult; + if ((verificationResult = PKCS7_verify(pkcs7, NULL, x509Store, NULL, receiptContents, 0)) != 1) { break; } + if (ERR_get_error() != 0) { break; } + + // Parse ASN.1 contents + ASN1_OCTET_STRING *asn1; + + if ((asn1 = pkcs7->d.sign->contents->d.data) != NULL) + { + [[[OCASN1 alloc] initWithData:asn1->data length:asn1->length] parseSetsOfSequencesWithContainerProvider:nil interpreter:^OCLicenseAppStoreReceiptParseError(id container, OCLicenseAppStoreReceiptFieldType fieldType, OCASN1 *contents) { + OCLicenseAppStoreReceiptParseError error = OCLicenseAppStoreReceiptParseErrorNone; + + switch (fieldType) + { + case OCLicenseAppStoreReceiptFieldTypeAppBundleIdentifier: + self->_appBundleIdentifier = contents.UTF8String; + break; + + case OCLicenseAppStoreReceiptFieldTypeAppVersion: + self->_appVersion = contents.UTF8String; + break; + + case OCLicenseAppStoreReceiptFieldTypeAppOriginalVersion: + self->_originalAppVersion = contents.UTF8String; + break; + + case OCLicenseAppStoreReceiptFieldTypeReceiptCreationDate: + self->_creationDate = contents.RFC3339Date; + break; + + case OCLicenseAppStoreReceiptFieldTypeReceiptExpirationDate: + self->_expirationDate = contents.RFC3339Date; + break; + + case OCLicenseAppStoreReceiptFieldTypeInAppPurchase: + error = [contents parseSetsOfSequencesWithContainerProvider:^id{ + return ([OCLicenseAppStoreReceiptInAppPurchase new]); + } interpreter:^OCLicenseAppStoreReceiptParseError(OCLicenseAppStoreReceiptInAppPurchase *iap, OCLicenseAppStoreReceiptFieldType fieldType, OCASN1 *contents) { + + return ([iap parseField:fieldType withContents:contents]); + }]; + + self->_inAppPurchases = (self->_inAppPurchases != nil) ? [self->_inAppPurchases arrayByAddingObjectsFromArray:contents.containers] : contents.containers; + break; + + default: + break; + } + + return (error); + }]; + } + + // DONE! + error = OCLicenseAppStoreReceiptParseErrorNone; + }while(NO); + + + // Free resources + if (receiptContents!=NULL) + { + BIO_free(receiptContents); + receiptContents = NULL; + } + + if (x509!=NULL) + { + X509_free(x509); + x509 = NULL; + } + + if (x509Store!=NULL) + { + X509_STORE_free(x509Store); + x509Store = NULL; + } + + if (pkcs7!=NULL) + { + PKCS7_free(pkcs7); + pkcs7 = NULL; + } + + EVP_cleanup(); // "Remove all ciphers and digests from the table" + + return (error); +} + +- (NSString *)description +{ + return ([NSString stringWithFormat:@"<%@: %p, receiptData: %@, creationDate: %@, expirationDate: %@, appBundleIdentifier: %@, appVersion: %@, originalAppVersion: %@, inAppPurchases: %@>", NSStringFromClass(self.class), self, _receiptData, _creationDate, _expirationDate, _appBundleIdentifier, _appVersion, _originalAppVersion, _inAppPurchases]); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.h b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.h new file mode 100644 index 000000000..29825dbc2 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.h @@ -0,0 +1,48 @@ +// +// OCLicenseAppStoreReceiptInAppPurchase.h +// ownCloudApp +// +// Created by Felix Schwarz on 03.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseAppStoreReceipt.h" + +@class OCASN1; + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseAppStoreReceiptInAppPurchase : NSObject + +@property(nullable,readonly,strong) NSNumber *quantity; +@property(nullable,readonly,strong) OCLicenseAppStoreProductIdentifier productID; + +@property(nullable,readonly,strong) NSDate *purchaseDate; +@property(nullable,readonly,strong) NSDate *originalPurchaseDate; + +@property(nullable,readonly,strong) NSDate *cancellationDate; + +@property(nullable,readonly,strong) NSDate *subscriptionExpirationDate; +@property(nullable,readonly,strong) NSNumber *subscriptionInIntroOfferPeriod; + +@property(nullable,readonly,strong) OCLicenseAppStoreLineItemID webOrderLineItemID; + +@property(nullable,readonly,strong) OCLicenseAppStoreTransactionID transactionID; +@property(nullable,readonly,strong) OCLicenseAppStoreTransactionID originalTransactionID; + +- (OCLicenseAppStoreReceiptParseError)parseField:(OCLicenseAppStoreReceiptFieldType)fieldType withContents:(OCASN1 *)contents; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.m b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.m new file mode 100644 index 000000000..9197809dd --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/App Store/Receipt/OCLicenseAppStoreReceiptInAppPurchase.m @@ -0,0 +1,80 @@ +// +// OCLicenseAppStoreReceiptInAppPurchase.m +// ownCloudApp +// +// Created by Felix Schwarz on 03.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseAppStoreReceiptInAppPurchase.h" +#import "OCASN1.h" + +@implementation OCLicenseAppStoreReceiptInAppPurchase + +- (OCLicenseAppStoreReceiptParseError)parseField:(OCLicenseAppStoreReceiptFieldType)fieldType withContents:(OCASN1 *)contents +{ + switch (fieldType) + { + case OCLicenseAppStoreReceiptFieldTypeIAPQuantity: + _quantity = contents.integer; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPProductID: + _productID = contents.UTF8String; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPPurchaseDate: + _purchaseDate = contents.RFC3339Date; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPOriginalPurchaseDate: + _originalPurchaseDate = contents.RFC3339Date; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPSubscriptionExpirationDate: + _subscriptionExpirationDate = contents.RFC3339Date; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPTransactionID: + self->_transactionID = contents.UTF8String; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPOriginalTransactionID: + self->_originalTransactionID = contents.UTF8String; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPCancellationDate: + self->_cancellationDate = contents.RFC3339Date; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPWebOrderLineItemID: + self->_webOrderLineItemID = contents.integer; + break; + + case OCLicenseAppStoreReceiptFieldTypeIAPSubscriptionInIntroOfferPeriod: + self->_subscriptionInIntroOfferPeriod = contents.integer; + break; + + default: + break; + } + + return (OCLicenseAppStoreReceiptParseErrorNone); +} + +- (NSString *)description +{ + return ([NSString stringWithFormat:@"<%@: %p, quantity: %@, productID: %@, purchaseDate: %@, originalPurchaseDate: %@, cancellationDate: %@, subscriptionExpirationDate: %@, subscriptionInIntroOfferPeriod: %@, webOrderLineItemID: %@, transactionID: %@, originalTransactionID: %@>", NSStringFromClass(self.class), self, _quantity, _productID, _purchaseDate, _originalPurchaseDate, _cancellationDate, _subscriptionExpirationDate, _subscriptionInIntroOfferPeriod, _webOrderLineItemID, _transactionID, _originalTransactionID]); +} + +@end diff --git a/ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.h b/ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.h new file mode 100644 index 000000000..baddd18b8 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.h @@ -0,0 +1,33 @@ +// +// OCLicenseEnterpriseProvider.h +// ownCloudApp +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCLicenseEnterpriseProvider : OCLicenseProvider + +@property(strong,readonly) NSArray *unlockedProductIdentifiers; + +- (instancetype)initWithUnlockedProductIdentifiers:(NSArray *)unlockedProductIdentifiers; + +@end + +extern OCLicenseProviderIdentifier OCLicenseProviderIdentifierEnterprise; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.m b/ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.m new file mode 100644 index 000000000..b4c95c138 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/Enterprise/OCLicenseEnterpriseProvider.m @@ -0,0 +1,57 @@ +// +// OCLicenseEnterpriseProvider.m +// ownCloudApp +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseEntitlement.h" +#import "OCLicenseEnterpriseProvider.h" + +@implementation OCLicenseEnterpriseProvider + +#pragma mark - Init +- (instancetype)initWithUnlockedProductIdentifiers:(NSArray *)unlockedProductIdentifiers +{ + if ((self = [super initWithIdentifier:OCLicenseProviderIdentifierEnterprise]) != nil) + { + _unlockedProductIdentifiers = unlockedProductIdentifiers; + self.localizedName = OCLocalized(@"Enterprise"); + } + + return (self); +} + +- (void)startProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + NSMutableArray *entitlements = [NSMutableArray new]; + + for (OCLicenseProductIdentifier productIdentifier in self.unlockedProductIdentifiers) + { + OCLicenseEntitlement *entitlement; + + entitlement = [OCLicenseEntitlement entitlementWithIdentifier:nil forProduct:productIdentifier type:OCLicenseTypePurchase valid:YES expiryDate:nil applicability:@"core.connection.serverEdition == \"Enterprise\" || bookmark.userInfo.statusInfo.edition == \"Enterprise\""]; + + [entitlements addObject:entitlement]; + } + + self.entitlements = (entitlements.count > 0) ? entitlements : nil; + + completionHandler(self, nil); +} + +@end + +OCLicenseProviderIdentifier OCLicenseProviderIdentifierEnterprise = @"enterprise"; diff --git a/ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.h b/ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.h new file mode 100644 index 000000000..f09e740da --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.h @@ -0,0 +1,59 @@ +// +// OCLicenseProvider.h +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" + +@class OCLicenseManager; +@class OCLicenseOffer; +@class OCLicenseEntitlement; +@class OCLicenseProvider; +@class OCLicenseTransaction; + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^OCLicenseProviderCompletionHandler)(OCLicenseProvider *provider, NSError * _Nullable error); + +@interface OCLicenseProvider : NSObject + +@property(weak,nullable) OCLicenseManager *manager; + +- (instancetype)initWithIdentifier:(OCLicenseProviderIdentifier)identifier; + +#pragma mark - Metadata +@property(strong) OCLicenseProviderIdentifier identifier; //!< Identifier uniquely identifying this license provider +@property(nullable,strong) NSString *localizedName; //!< (optional) localized name of the license provider + +#pragma mark - Storage +@property(strong,nonatomic,nullable) NSURL *storageURL; //!< If .manager is not nil, URL pointing to a filesystem location where the provider can persist/cache its data. +@property(strong,nonatomic,nullable) NSData *storedData; //!< Convenience wrapper that loads/saves data from/to .storageURL + +#pragma mark - Payload +@property(nullable,strong,nonatomic) NSArray *offers; //!< Offers made available by the provider. Updates to this property trigger updates in OCLicenseManager. +@property(nullable,strong,nonatomic) NSArray *entitlements; //!< Entitlements found by the provider. Updates to this property trigger updates in OCLicenseManager. + +#pragma mark - Transaction access +- (void)retrieveTransactionsWithCompletionHandler:(void(^)(NSError * _Nullable error, NSArray * _Nullable transactions))completionHandler; //!< Retrieve transactions + +#pragma mark - Control +- (void)startProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler; //!< Called when the provider should start providing payloads +- (void)stopProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler; //!< Called when the provider should stop providing payloads + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.m b/ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.m new file mode 100644 index 000000000..e91b9ada7 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Providers/OCLicenseProvider.m @@ -0,0 +1,114 @@ +// +// OCLicenseProvider.m +// ownCloudApp +// +// Created by Felix Schwarz on 29.10.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseProvider.h" +#import "OCLicenseEntitlement.h" +#import "OCLicenseOffer.h" + +@implementation OCLicenseProvider + +- (instancetype)initWithIdentifier:(OCLicenseProviderIdentifier)identifier +{ + if ((self = [super init]) != nil) + { + _identifier = identifier; + } + + return (self); +} + +- (void)setEntitlements:(NSArray *)entitlements +{ + for (OCLicenseEntitlement *entitlement in _entitlements) + { + if ([entitlements indexOfObjectIdenticalTo:entitlement] == NSNotFound) + { + entitlement.provider = nil; + } + } + + for (OCLicenseEntitlement *entitlement in entitlements) + { + entitlement.provider = self; + } + + _entitlements = entitlements; +} + +- (void)setOffers:(NSArray *)offers +{ + for (OCLicenseOffer *offer in _offers) + { + if ([offers indexOfObjectIdenticalTo:offer] == NSNotFound) + { + offer.provider = nil; + } + } + + for (OCLicenseOffer *offer in offers) + { + offer.provider = self; + } + + _offers = offers; +} + +#pragma mark - Transaction access +- (void)retrieveTransactionsWithCompletionHandler:(void (^)(NSError * _Nullable, NSArray * _Nullable))completionHandler +{ + completionHandler(nil, nil); +} + +#pragma mark - Control +- (void)startProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + +} + +- (void)stopProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + +} + +#pragma mark - Storage +- (NSData *)storedData +{ + if (self.storageURL != nil) + { + return ([NSData dataWithContentsOfURL:self.storageURL]); + } + + return (nil); +} + +- (void)setStoredData:(NSData *)storedData +{ + if (self.storageURL != nil) + { + if (storedData != nil) + { + [storedData writeToURL:self.storageURL atomically:YES]; + } + else + { + [NSFileManager.defaultManager removeItemAtURL:self.storageURL error:NULL]; + } + } +} + +@end diff --git a/ownCloudAppFramework/Licensing/README.md b/ownCloudAppFramework/Licensing/README.md new file mode 100644 index 000000000..ae1515e6f --- /dev/null +++ b/ownCloudAppFramework/Licensing/README.md @@ -0,0 +1,122 @@ +# Licensing + +## Overview + +The `OCLicense` set of classes allow gating and granting access to features through an extensible number of different mechanisms through a single, unified interface: + +- `OCLicenseFeature` represents a particular feature for which access is gated. + +- `OCLicenseProduct` represents a product and is defined by a collection of features. + - for an IAP unlocking a *single feature*, that product would be defined by that single feature + - for an *Unlock all* IAP, that product would be defined by *all* features + - this allows creating tailored *products* consisting of a particular feature set, representing actual products + +- `OCLicenseEnvironment` encapsulates information on an environment against which the authorization to use a product should be checked + - typically defined by host name, TLS certificate, etc. + +- `OCLicenseEntitlement` represents the entitlement to use a product. An entitlement + - identifies its origin: where does it come from? + - includes an `expiryDate` property (to allow trials + subscription expirations) + - provides information on *validity* and *applicability*: + - *validity*: if this entitlement should be considered at all (i.e. has not expired) + - *applicability*: if this entitlement actually authorizes the use of a product in a certain `OCLicenseEnvironment` + - can limit the authorization to use a product/feature to a certain domain/TLS certificate/public key + +- `OCLicenseOffer` represents an offer to purchase a product + +- `OCLicenseTransaction` represent a transaction and provide information on: + - quantity and product + - type of transaction (trial, subscription, etc.) + - transaction date + - expiration date + - on purpose: **does not** contain financial details + +- `OCLicenseProvider` retrieve and provide information + - about licensed/purchased products in the form of `OCLicenseEntitlement`s, sourced from f.ex. + - In App Purchases + - Subscriptions + - License Information pulled from a server + - App Store Receipt original purchase date + - about offers in the form of `OCLicenseOffer`s, sourced from f.ex. + - StoreKit (App Store) + - about transactions in the form of `OCLicenseTransaction`s, sourced from f.ex. + - App Store Receipt IAPs + - OC server license endpoint + +- `OCLicenseManager` + - puts all these pieces together and provides APIs to determine if the usage of a certain feature is allowed + - allows observation of single or groups of products and features in a particular environment and notify on change (handled through `OCLicenseObserver`) + +## Hierarchy +- Sessions + - `OCLicenseEnvironment` +- `OCLicenseManager` + + - `OCLicenseFeature`s + - `OCLicenseProduct`s + - `OCLicenseProvider`s + - `OCLicenseEntitlement`s + - `OCLicenseOffer`s + - `OCLicenseTransaction`s + - `OCLicenseObserver` + - app code + +## Examples + +#### Registering features and products +```objc +// Register features +[OCLicenseManger.sharedLicenseManager registerFeature:[OCLicenseFeature featureWithIdentifier:@"feature.document-scanning" localizedName:@"Document scanning" localizedDescription:nil]]; +[OCLicenseManger.sharedLicenseManager registerFeature:[OCLicenseFeature featureWithIdentifier:@"feature.shortcuts" localizedName:@"Shortcuts" localizedDescription:nil]]; + +// Register products +[OCLicenseManger.sharedLicenseManager registerProduct:[OCLicenseProduct productWithIdentifier:@"product.document-scanner" localizedName:@"Document scanner" contents:@[ + @"feature.document-scanning" +]]]; + +[OCLicenseManger.sharedLicenseManager registerProduct:[OCLicenseProduct productWithIdentifier:@"product.shortcuts" localizedName:@"Shortcuts" contents:@[ + @"feature.shortcuts" +]]]; + +[OCLicenseManger.sharedLicenseManager registerProduct:[OCLicenseProduct productWithIdentifier:@"product.unlock-all" localizedName:@"Unlock all" contents:@[ + @"feature.document-scanning", + @"feature.shortcuts" +]]]; +``` + +#### Determining state and reacting to changes +Determine current status: +```objc +OCLicenseAuthorizationStatus status = [OCLicenseManager.sharedLicenseManager authorizationStatusForFeature:@"feature.document-scanning" inEnvironment:core.environment]; + +if (status == OCLicenseAuthorizationStatusGranted) +{ + // Feature available +} +else +{ + // Feature not available +} +``` + +Determine current (initial) status and subscribe to status changes: +```objc +[OCLicenseManager.sharedLicenseManager observeProducts:nil features:@[ @"feature.document-scanning" ] environment:core.environment withOwner:self updateHandler:^(OCLicenseObserver *observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus){ + // Handle updates to authorization status to use the document scanner feature +}]; +``` + +## Setup + +To comply with `StoreKit` requirements, the `OCLicense` system needs to +- be completely set up in the `-[UIApplicationDelegate application:didFinishLaunchingWithOptions:]` method. In particular, the `OCLicenseAppStoreProvider` method needs to be added to `OCLicenseManager` in that method. + +## Reference +### App Store Receipt parsing +- RevenueCat: [Dissecting an App Store Receipt](https://www.revenuecat.com/2018/01/17/dissecting-an-app-store-receipt) +- Apple: [Validating Receipts Locally](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html) +- Apple: [Receipt Fields](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1) + +### Non-consumable IAPs as trial mechanism +- MacRumors: [Free Trials for All Paid Apps Now Possible Thanks to Updated App Store Guidelines](https://www.macrumors.com/2018/06/05/app-store-app-free-trials-now-available/) +- Apple: [App Store Review Guidelines: In-App Purchases](https://developer.apple.com/app-store/review/guidelines/#in-app-purchase) diff --git a/ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.h b/ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.h new file mode 100644 index 000000000..1e64dde1b --- /dev/null +++ b/ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.h @@ -0,0 +1,58 @@ +// +// OCLicenseTransaction.h +// ownCloudApp +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCLicenseTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OCLicenseProvider; +@class OCLicenseProduct; + +typedef NSString* OCLicenseTransactionIdentifier; + +@interface OCLicenseTransaction : NSObject + +@property(nullable,weak) OCLicenseProvider *provider; + +@property(nullable,strong) OCLicenseTransactionIdentifier identifier; //!< Transaction ID + +@property(assign) OCLicenseType type; //!< Type + +@property(assign) NSInteger quantity; //!< Quantity +@property(nullable,strong) NSString *name; //!< Name of item (typically product name) + +@property(nullable,strong) OCLicenseProductIdentifier productIdentifier; +@property(nullable,strong,nonatomic,readonly) OCLicenseProduct *product; + +@property(nullable,strong) NSDate *date; +@property(nullable,strong) NSDate *endDate; +@property(nullable,strong) NSDate *cancellationDate; + +@property(nullable,strong,nonatomic) NSArray *> *tableRows; +@property(nullable,strong,nonatomic) NSArray *> *displayTableRows; + +@property(nullable,strong) NSDictionary *links; + ++ (instancetype)transactionWithProvider:(nullable OCLicenseProvider *)provider identifier:(OCLicenseTransactionIdentifier)identifier type:(OCLicenseType)type quantity:(NSInteger)quantity name:(NSString *)name productIdentifier:(nullable OCLicenseProductIdentifier)productIdentifier date:(nullable NSDate *)date endDate:(nullable NSDate *)endDate cancellationDate:(nullable NSDate *)cancellationDate; + ++ (instancetype)transactionWithProvider:(nullable OCLicenseProvider *)provider tableRows:(NSArray *> *)tableRows; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.m b/ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.m new file mode 100644 index 000000000..9f3382cd4 --- /dev/null +++ b/ownCloudAppFramework/Licensing/Transactions/OCLicenseTransaction.m @@ -0,0 +1,168 @@ +// +// OCLicenseTransaction.m +// ownCloudApp +// +// Created by Felix Schwarz on 05.12.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCLicenseTransaction.h" +#import "OCLicenseManager.h" +#import "OCLicenseProvider.h" +#import "OCLicenseProduct.h" + +@implementation OCLicenseTransaction + ++ (instancetype)transactionWithProvider:(nullable OCLicenseProvider *)provider identifier:(OCLicenseTransactionIdentifier)identifier type:(OCLicenseType)type quantity:(NSInteger)quantity name:(NSString *)name productIdentifier:(nullable OCLicenseProductIdentifier)productIdentifier date:(nullable NSDate *)date endDate:(nullable NSDate *)endDate cancellationDate:(nullable NSDate *)cancellationDate +{ + OCLicenseTransaction *transaction = [OCLicenseTransaction new]; + + transaction.provider = provider; + + transaction.identifier = identifier; + transaction.type = type; + + transaction.quantity = quantity; + transaction.name = name; + + transaction.productIdentifier = productIdentifier; + transaction.date = date; + transaction.endDate = endDate; + transaction.cancellationDate = cancellationDate; + + return (transaction); +} + ++ (instancetype)transactionWithProvider:(nullable OCLicenseProvider *)provider tableRows:(NSArray *> *)tableRows +{ + OCLicenseTransaction *transaction = [OCLicenseTransaction new]; + + transaction.provider = provider; + transaction.tableRows = tableRows; + + return (transaction); +} + ++ (NSDateFormatter *)localizedDateFormatter +{ + static NSDateFormatter *dateFormatter; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + + dateFormatter.dateStyle = NSDateFormatterMediumStyle; + dateFormatter.timeStyle = NSDateFormatterMediumStyle; + dateFormatter.locale = NSLocale.currentLocale; + }); + + return (dateFormatter); +} + +- (OCLicenseProduct *)product +{ + return ([self.provider.manager productWithIdentifier:self.productIdentifier]); +} + +- (NSArray *> *)tableRows +{ + if (_tableRows == nil) + { + NSMutableArray *> *tableRows = [@[ + @{ @"Type" : [OCLicenseProduct stringForType:self.type] }, + @{ @"Quantity" : @(self.quantity) } + ] mutableCopy]; + + if (_name != nil) + { + [tableRows insertObject:@{ + @"Product" : _name + } atIndex:0]; + } + + if (_date != nil) + { + [tableRows addObject:@{ + @"Date" : _date + }]; + } + + if (_cancellationDate != nil) + { + [tableRows addObject:@{ + @"Cancelled" : _cancellationDate + }]; + } + + if (_endDate != nil) + { + [tableRows addObject:@{ + @"Ends" : _endDate + }]; + } + + _tableRows = tableRows; + } + + return (_tableRows); +} + +- (NSArray *> *)displayTableRows +{ + if (_displayTableRows == nil) + { + NSArray *> *tableRows = nil; + + if (((tableRows = self.tableRows) != nil) && (tableRows.count > 0)) + { + NSMutableArray *> *displayTableRows = [NSMutableArray new]; + + for (NSDictionary *tableRow in tableRows) + { + NSMutableDictionary *row = [NSMutableDictionary new]; + + [tableRow enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + if ([obj isKindOfClass:NSNumber.class]) + { + obj = ((NSNumber *)obj).stringValue; + } + + if ([obj isKindOfClass:NSDate.class]) + { + obj = [[OCLicenseTransaction localizedDateFormatter] stringFromDate:obj]; + } + + if (![obj isKindOfClass:NSString.class]) + { + obj = ((NSObject *)obj).description; + } + + row[OCLocalized(key)] = obj; + }]; + + [displayTableRows addObject:row]; + } + + _displayTableRows = displayTableRows; + } + } + + return (_displayTableRows); +} + +- (NSString *)description +{ + return ([NSString stringWithFormat:@"<%@: %p, %@>", NSStringFromClass(self.class), self, self.displayTableRows]); +} + +@end diff --git a/ownCloudAppFramework/Resources/en.lproj/Localized.strings b/ownCloudAppFramework/Resources/en.lproj/Localized.strings new file mode 100644 index 000000000..be3b757d6 --- /dev/null +++ b/ownCloudAppFramework/Resources/en.lproj/Localized.strings @@ -0,0 +1,9 @@ +/* + Localized.strings + ownCloud + + Created by Felix Schwarz on 31.01.20. + Copyright © 2020 ownCloud GmbH. All rights reserved. +*/ + +"Purchases are not allowed on this device." = "Purchases are not allowed on this device."; diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 5bb1d638b..fd17c5ea9 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -31,3 +31,26 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import #import #import + +#import +#import +#import + +#import +#import + +#import +#import +#import +#import +#import + +#import +#import +#import +#import + +#import + +#import +#import diff --git a/ownCloudAppFrameworkTests/LicensingTests.m b/ownCloudAppFrameworkTests/LicensingTests.m new file mode 100644 index 000000000..e074d2b46 --- /dev/null +++ b/ownCloudAppFrameworkTests/LicensingTests.m @@ -0,0 +1,486 @@ +// +// LicensingTests.m +// LicensingTests +// +// Created by Felix Schwarz on 21.11.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +#import +#import + +@interface LicensingTests : XCTestCase + +@end + +typedef void(^LicenseProviderBlock)(OCLicenseProvider *provider, OCLicenseProviderCompletionHandler completionHandler); + +@interface TestProvider : OCLicenseProvider + +@property(copy) LicenseProviderBlock startBlock; +@property(copy) LicenseProviderBlock stopBlock; + +@end + +@implementation TestProvider + +- (void)startProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + if (self.startBlock != nil) + { + self.startBlock(self, completionHandler); + } +} + +- (void)stopProvidingWithCompletionHandler:(OCLicenseProviderCompletionHandler)completionHandler +{ + if (self.stopBlock != nil) + { + self.stopBlock(self, completionHandler); + } +} + +@end + +@implementation LicensingTests + +- (void)_registerFeaturesAndProductsInManager:(OCLicenseManager *)manager +{ + // Register features + [manager registerFeature:[OCLicenseFeature featureWithIdentifier:@"feature-1"]]; + [manager registerFeature:[OCLicenseFeature featureWithIdentifier:@"feature-2"]]; + + // Register products + [manager registerProduct:[OCLicenseProduct productWithIdentifier:@"single.feature-1" name:@"Feature 1" description:@"Unlock Feature 1" contents:@[ + @"feature-1" + ]]]; + + [manager registerProduct:[OCLicenseProduct productWithIdentifier:@"single.feature-2" name:@"Feature 2" description:@"Unlock Feature 2" contents:@[ + @"feature-2" + ]]]; + + [manager registerProduct:[OCLicenseProduct productWithIdentifier:@"bundle.feature-1-2" name:@"Both Features" description:@"Unlock Both Features" contents:@[ + @"feature-1", + @"feature-2" + ]]]; +} + +- (void)testFeatureContainedInProductsAssociation +{ + XCTestExpectation *expectF1Single1 = [self expectationWithDescription:@"Expect feature-1 in single.feature-1"]; + XCTestExpectation *expectF1Bundle = [self expectationWithDescription:@"Expect feature-1 in bundle.feature-1-2"]; + XCTestExpectation *expectF2Single2 = [self expectationWithDescription:@"Expect feature-2 in single.feature-2"]; + XCTestExpectation *expectF2Bundle = [self expectationWithDescription:@"Expect feature-2 in bundle.feature-1-2"]; + + OCLicenseManager *manager = [OCLicenseManager new]; + + [self _registerFeaturesAndProductsInManager:manager]; + + [manager.queue async:^(dispatch_block_t _Nonnull completionHandler) { + for (OCLicenseProduct *product in [manager featureWithIdentifier:@"feature-1"].containedInProducts) + { + if ([product.identifier isEqualToString:@"single.feature-1"]) + { + [expectF1Single1 fulfill]; + } + else if ([product.identifier isEqualToString:@"bundle.feature-1-2"]) + { + [expectF1Bundle fulfill]; + } + else + { + XCTFail(@"Unexpected product %@ for feature 1", product.identifier); + } + } + + for (OCLicenseProduct *product in [manager featureWithIdentifier:@"feature-2"].containedInProducts) + { + if ([product.identifier isEqualToString:@"single.feature-2"]) + { + [expectF2Single2 fulfill]; + } + else if ([product.identifier isEqualToString:@"bundle.feature-1-2"]) + { + [expectF2Bundle fulfill]; + } + else + { + XCTFail(@"Unexpected product %@ for feature 2", product.identifier); + } + } + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testLicenseProviderStartStop +{ + XCTestExpectation *expectStart = [self expectationWithDescription:@"Expect start"]; + XCTestExpectation *expectStop = [self expectationWithDescription:@"Expect stop"]; + + OCLicenseManager *manager = [OCLicenseManager new]; + TestProvider *provider = [TestProvider new]; + + provider.startBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + [expectStart fulfill]; + }; + + provider.stopBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + [expectStop fulfill]; + }; + + [self _registerFeaturesAndProductsInManager:manager]; + + [manager addProvider:provider]; + [manager removeProvider:provider]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testUnlock +{ + XCTestExpectation *expectFeature1PermissionDenied = [self expectationWithDescription:@"Expect F1 permission denied"]; + XCTestExpectation *expectFeature1PermissionGranted = [self expectationWithDescription:@"Expect F1 permission granted"]; + XCTestExpectation *expectProduct1PermissionDenied = [self expectationWithDescription:@"Expect P1 permission denied"]; + XCTestExpectation *expectProduct1PermissionGranted = [self expectationWithDescription:@"Expect P1 permission granted"]; + + XCTestExpectation *expectFeature2PermissionDenied = [self expectationWithDescription:@"Expect F2 permission denied"]; + XCTestExpectation *expectFeature2PermissionGranted = [self expectationWithDescription:@"Expect F2 permission granted"]; + XCTestExpectation *expectProduct2PermissionDenied = [self expectationWithDescription:@"Expect P2 permission denied"]; + XCTestExpectation *expectProduct2PermissionGranted = [self expectationWithDescription:@"Expect P2 permission granted"]; + + OCLicenseManager *manager = [OCLicenseManager new]; + OCLicenseEnvironment *environment = [OCLicenseEnvironment environmentWithIdentifier:@"environment" hostname:@"demo.owncloud.org" certificate:nil attributes:nil]; + TestProvider *provider = [TestProvider new]; + + [self _registerFeaturesAndProductsInManager:manager]; + + provider.startBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + provider.entitlements = @[ + [OCLicenseEntitlement entitlementWithIdentifier:nil forProduct:@"single.feature-1" type:OCLicenseTypePurchase valid:YES expiryDate:nil applicability:nil] + ]; + + completionHandler(provider, nil); + }; + + provider.stopBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + provider.entitlements = nil; + + completionHandler(provider, nil); + }; + + dispatch_group_t addEntitlementGroup = dispatch_group_create(); + + // Observe feature 1 + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:nil features:@[ @"feature-1" ] inEnvironment:environment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectFeature1PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectFeature1PermissionGranted fulfill]; + } + }]; + + // Observe product 1 + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:@[ @"single.feature-1" ] features:nil inEnvironment:environment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectProduct1PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectProduct1PermissionGranted fulfill]; + } + }]; + + // Observe feature 2 + expectFeature2PermissionGranted.inverted = YES; + + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:nil features:@[ @"feature-2" ] inEnvironment:environment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectFeature2PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectFeature2PermissionGranted fulfill]; + } + }]; + + // Observe product 2 + expectProduct2PermissionGranted.inverted = YES; + + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:@[ @"single.feature-2" ] features:nil inEnvironment:environment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectProduct2PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectProduct2PermissionGranted fulfill]; + } + }]; + + // Add provider once all observers received their denied status update + dispatch_group_notify(addEntitlementGroup, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [manager addProvider:provider]; + }); + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testUnlockWithLimitedApplicability +{ + XCTestExpectation *expectFeature1PermissionDenied = [self expectationWithDescription:@"Expect F1 permission denied"]; + XCTestExpectation *expectFeature1PermissionGranted = [self expectationWithDescription:@"Expect F1 permission granted"]; + + XCTestExpectation *expectProduct1PermissionDenied = [self expectationWithDescription:@"Expect P1 permission denied"]; + XCTestExpectation *expectProduct1PermissionGranted = [self expectationWithDescription:@"Expect P1 permission granted"]; + + OCLicenseManager *manager = [OCLicenseManager new]; + OCLicenseEnvironment *orgEnvironment = [OCLicenseEnvironment environmentWithIdentifier:@"org" hostname:@"demo.owncloud.org" certificate:nil attributes:nil]; + OCLicenseEnvironment *comEnvironment = [OCLicenseEnvironment environmentWithIdentifier:@"com" hostname:@"demo.owncloud.com" certificate:nil attributes:nil]; + TestProvider *provider = [TestProvider new]; + + [self _registerFeaturesAndProductsInManager:manager]; + + provider.startBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + provider.entitlements = @[ + [OCLicenseEntitlement entitlementWithIdentifier:nil forProduct:@"single.feature-1" type:OCLicenseTypePurchase valid:YES expiryDate:nil applicability:@"identifier = \"com\""] + ]; + + completionHandler(provider, nil); + }; + + provider.stopBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + provider.entitlements = nil; + + completionHandler(provider, nil); + }; + + dispatch_group_t addEntitlementGroup = dispatch_group_create(); + + // Observe feature 1 in orgEnvironment + dispatch_group_enter(addEntitlementGroup); + + expectFeature1PermissionGranted.inverted = YES; + + [manager observeProducts:nil features:@[ @"feature-1" ] inEnvironment:orgEnvironment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectFeature1PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectFeature1PermissionGranted fulfill]; + } + }]; + + // Observe product 1 in comEnvironment + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:@[ @"single.feature-1" ] features:nil inEnvironment:comEnvironment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectProduct1PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectProduct1PermissionGranted fulfill]; + } + }]; + + // Add provider once all observers received their denied status update + dispatch_group_notify(addEntitlementGroup, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [manager addProvider:provider]; + }); + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testUnlockWithExpiration +{ + XCTestExpectation *expectFeature1PermissionDenied = [self expectationWithDescription:@"Expect F1 permission denied"]; + XCTestExpectation *expectFeature1PermissionGranted = [self expectationWithDescription:@"Expect F1 permission granted"]; + XCTestExpectation *expectFeature1PermissionExpired = [self expectationWithDescription:@"Expect F1 permission denied again"]; + + XCTestExpectation *expectProduct1PermissionDenied = [self expectationWithDescription:@"Expect P1 permission denied"]; + XCTestExpectation *expectProduct1PermissionGranted = [self expectationWithDescription:@"Expect P1 permission granted"]; + XCTestExpectation *expectProduct1PermissionExpired = [self expectationWithDescription:@"Expect P1 permission denied again"]; + + OCLicenseManager *manager = [OCLicenseManager new]; + OCLicenseEnvironment *environment = [OCLicenseEnvironment environmentWithIdentifier:@"environment" hostname:@"demo.owncloud.org" certificate:nil attributes:nil]; + TestProvider *provider = [TestProvider new]; + + [self _registerFeaturesAndProductsInManager:manager]; + + provider.startBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + provider.entitlements = @[ + [OCLicenseEntitlement entitlementWithIdentifier:nil forProduct:@"single.feature-1" type:OCLicenseTypePurchase valid:YES expiryDate:[NSDate dateWithTimeIntervalSinceNow:3] applicability:nil] + ]; + + completionHandler(provider, nil); + }; + + provider.stopBlock = ^(OCLicenseProvider *provider, void (^completionHandler)(OCLicenseProvider *provider, NSError * _Nullable error)) { + provider.entitlements = nil; + + completionHandler(provider, nil); + }; + + dispatch_group_t addEntitlementGroup = dispatch_group_create(); + + // Observe feature 1 + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:nil features:@[ @"feature-1" ] inEnvironment:environment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectFeature1PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectFeature1PermissionGranted fulfill]; + } + + if (authorizationStatus == OCLicenseAuthorizationStatusExpired) + { + [expectFeature1PermissionExpired fulfill]; + } + }]; + + // Observe product 1 + dispatch_group_enter(addEntitlementGroup); + [manager observeProducts:@[ @"single.feature-1" ] features:nil inEnvironment:environment withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, OCLicenseAuthorizationStatus authorizationStatus) { + if (authorizationStatus == OCLicenseAuthorizationStatusDenied) + { + [expectProduct1PermissionDenied fulfill]; + dispatch_group_leave(addEntitlementGroup); + } + + if (authorizationStatus == OCLicenseAuthorizationStatusGranted) + { + [expectProduct1PermissionGranted fulfill]; + } + + if (authorizationStatus == OCLicenseAuthorizationStatusExpired) + { + [expectProduct1PermissionExpired fulfill]; + } + }]; + + // Add provider once all observers received their denied status update + dispatch_group_notify(addEntitlementGroup, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [manager addProvider:provider]; + }); + + [self waitForExpectationsWithTimeout:10 handler:nil]; + +} + +- (NSArray *)_appStoreItems +{ + return (@[ + [OCLicenseAppStoreItem trialWithAppStoreIdentifier:@"trial.pro.30days" trialDuration:[[OCLicenseDuration alloc] initWithUnit:OCLicenseDurationUnitDay length:30] productIdentifier:@"bundle.pro"], + [OCLicenseAppStoreItem nonConsumableIAPWithAppStoreIdentifier:@"single.documentsharing" productIdentifier:@"single.documentsharing"], + [OCLicenseAppStoreItem subscriptionWithAppStoreIdentifier:@"bundle.pro" productIdentifier:@"bundle.pro" trialDuration:[[OCLicenseDuration alloc] initWithUnit:OCLicenseDurationUnitDay length:30]] + ]); +} + +- (void)_registerAppStoreFeaturesAndProductsInManager:(OCLicenseManager *)manager +{ + // Register features + [manager registerFeature:[OCLicenseFeature featureWithIdentifier:@"documentsharing"]]; + + // Register products + [manager registerProduct:[OCLicenseProduct productWithIdentifier:@"single.documentsharing" name:@"Document Sharing" description:@"Unlock Document Sharing" contents:@[ + @"documentsharing" + ]]]; + + [manager registerProduct:[OCLicenseProduct productWithIdentifier:@"bundle.pro" name:@"Pro Bundle" description:@"Unlock Pro Features" contents:@[ + @"documentsharing" + ]]]; +} + +- (void)testAppStoreProductRequest +{ + NSArray *appStoreItems = [self _appStoreItems]; + OCLicenseAppStoreProvider *provider = [[OCLicenseAppStoreProvider alloc] initWithItems:appStoreItems]; + + XCTestExpectation *expectResponse = [self expectationWithDescription:@"Expect response"]; + + [provider startProvidingWithCompletionHandler:^(OCLicenseProvider *provider, NSError * _Nullable error) { + OCLogDebug(@"error=%@, offers=%@", error, provider.offers); + + XCTAssert((error==nil), @"Error: %@", error); + XCTAssert((provider.offers!=nil), @"No offers!"); + XCTAssert((provider.offers.count==appStoreItems.count), @"Incomplete offers!"); + + [expectResponse fulfill]; + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testOfferObservation +{ + OCLicenseManager *manager = [OCLicenseManager new]; + NSArray *appStoreItems = [self _appStoreItems]; + OCLicenseAppStoreProvider *provider = [[OCLicenseAppStoreProvider alloc] initWithItems:appStoreItems]; + + XCTestExpectation *expectNoOffers = [self expectationWithDescription:@"Expect no offers"]; + __block XCTestExpectation *expectOffers = [self expectationWithDescription:@"Expect offers"]; + XCTestExpectation *expectNoOffersAgain = [self expectationWithDescription:@"Expect no offers again"]; + + [self _registerAppStoreFeaturesAndProductsInManager:manager]; + + [manager observeOffersForProducts:@[@"bundle.pro"] features:nil withOwner:self updateHandler:^(OCLicenseObserver * _Nonnull observer, BOOL isInitial, NSArray * _Nonnull offers) { + OCLogDebug(@"observer=%@, isInitial=%d, offers=%@", observer, isInitial, offers); + + if (isInitial) + { + [manager addProvider:provider]; + + if (offers.count == 0) + { + [expectNoOffers fulfill]; + } + } + else + { + if (offers.count > 0) + { + [expectOffers fulfill]; + expectOffers = nil; + + [manager removeProvider:provider]; + } + else if (expectOffers == nil) + { + [expectNoOffersAgain fulfill]; + } + } + }]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +@end diff --git a/ownCloudAppFrameworkTests/ownCloudAppTests.m b/ownCloudAppFrameworkTests/ownCloudAppTests.m deleted file mode 100644 index 65987b510..000000000 --- a/ownCloudAppFrameworkTests/ownCloudAppTests.m +++ /dev/null @@ -1,37 +0,0 @@ -// -// ownCloudAppTests.m -// ownCloudAppTests -// -// Created by Felix Schwarz on 21.05.19. -// Copyright © 2019 ownCloud GmbH. All rights reserved. -// - -#import - -@interface ownCloudAppTests : XCTestCase - -@end - -@implementation ownCloudAppTests - -- (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -- (void)testExample { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. -} - -- (void)testPerformanceExample { - // This is an example of a performance test case. - [self measureBlock:^{ - // Put the code you want to measure the time of here. - }]; -} - -@end diff --git a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition index a7c27a30a..dac070a4b 100644 --- a/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition +++ b/ownCloudAppShared/Intent/Base.lproj/Intents.intentdefinition @@ -214,11 +214,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID y8oko2 INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID VSQYEZ INIntentResponseCodeName @@ -609,11 +609,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID fjiVU2 INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID 0pF5fq INIntentResponseCodeName @@ -854,11 +854,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID rzbAOP INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID 8a8rsQ INIntentResponseCodeName @@ -1212,11 +1212,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID 81IjYk INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID yTzyi1 INIntentResponseCodeName @@ -1499,11 +1499,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID X0Zskf INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID bfU2dy INIntentResponseCodeName @@ -1740,11 +1740,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID idUnAy INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID lNNcvh INIntentResponseCodeName @@ -1987,11 +1987,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID 65dv1W INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID 9tIXKj INIntentResponseCodeName @@ -2253,11 +2253,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID sKUo1c INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID 1rpPPR INIntentResponseCodeName @@ -2408,11 +2408,11 @@ INIntentResponseCodeConciseFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeConciseFormatStringID ceG0fs INIntentResponseCodeFormatString - Your subscription is no longer valid. Please go into the app and renew the subscription for this feature. + This action is a Pro Feature and currently not unlocked. For more information and available unlocking options, please see Settings > Pro Features > Shortcuts. INIntentResponseCodeFormatStringID GJ25u2 INIntentResponseCodeName diff --git a/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift index c0c952bfe..a8465c23c 100644 --- a/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift +++ b/ownCloudAppShared/Intent/CreateFolderIntentHandler.swift @@ -25,12 +25,10 @@ public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { public func handle(intent: CreateFolderIntent, completion: @escaping (CreateFolderIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(CreateFolderIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(CreateFolderIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -47,6 +45,11 @@ public class CreateFolderIntentHandler: NSObject, CreateFolderIntentHandling { return } + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(CreateFolderIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in if error == nil, let targetItem = item { let folderPath = String(format: "%@%@", path, name) diff --git a/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift index a40c8c9db..e7e1299c9 100644 --- a/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift +++ b/ownCloudAppShared/Intent/DeletePathItemIntentHandler.swift @@ -25,12 +25,10 @@ public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling public func handle(intent: DeletePathItemIntent, completion: @escaping (DeletePathItemIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(DeletePathItemIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(DeletePathItemIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -47,6 +45,11 @@ public class DeletePathItemIntentHandler: NSObject, DeletePathItemIntentHandling return } + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(DeletePathItemIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in if error == nil, let targetItem = item, let core = core { let progress = core.delete(targetItem, requireMatch: true, resultHandler: { (error, _, _, _) in diff --git a/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift b/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift index 067e2434e..fbfe3776d 100644 --- a/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift +++ b/ownCloudAppShared/Intent/Extensions/OCBookmarkManager+Extension.swift @@ -40,14 +40,14 @@ extension OCBookmarkManager { return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first } - public func accountBookmark(for uuidString: String) -> Account? { + public func accountBookmark(for uuidString: String) -> (OCBookmark, Account)? { if let bookmark = bookmark(for: uuidString) { let account = Account(identifier: bookmark.uuid.uuidString, display: bookmark.shortName) account.name = bookmark.shortName account.serverURL = bookmark.url account.uuid = bookmark.uuid.uuidString - return account + return (bookmark, account) } return nil diff --git a/ownCloudAppShared/Intent/Extensions/OCLicenseManager+Setup.swift b/ownCloudAppShared/Intent/Extensions/OCLicenseManager+Setup.swift new file mode 100644 index 000000000..4e24a7cd0 --- /dev/null +++ b/ownCloudAppShared/Intent/Extensions/OCLicenseManager+Setup.swift @@ -0,0 +1,72 @@ +// +// OCLicenseManager+Setup.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 13.01.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import ownCloudApp + +public extension OCLicenseFeatureIdentifier { + static var documentScanner : OCLicenseFeatureIdentifier { return OCLicenseFeatureIdentifier(rawValue: "document-scanner") } + static var shortcuts : OCLicenseFeatureIdentifier { return OCLicenseFeatureIdentifier(rawValue: "shortcuts") } +} + +public extension OCLicenseProductIdentifier { + static var singleDocumentScanner : OCLicenseProductIdentifier { return OCLicenseProductIdentifier(rawValue: "single.document-scanner") } + static var singleShortcuts : OCLicenseProductIdentifier { return OCLicenseProductIdentifier(rawValue: "single.shortcuts") } + + static var bundlePro : OCLicenseProductIdentifier { return OCLicenseProductIdentifier(rawValue: "bundle.pro") } +} + +private var OCLicenseManagerHasBeenSetup : Bool = false + +public extension OCLicenseManager { + func setupLicenseManagement() { + if OCLicenseManagerHasBeenSetup { + return + } + + OCLicenseManagerHasBeenSetup = true + + // Set up features and products + let documentScannerFeature = OCLicenseFeature(identifier: .documentScanner, name: "Document Scanner".localized, description: "Scan documents and photos with your camera.".localized) + let shortcutsFeature = OCLicenseFeature(identifier: .shortcuts, name: "Shortcuts Actions".localized, description: "Use ownCloud actions in Shortcuts.".localized) + + // - Features + register(documentScannerFeature) + register(shortcutsFeature) + + // - Single feature products + register(OCLicenseProduct(identifier: .singleDocumentScanner, name: documentScannerFeature.localizedName!, description: documentScannerFeature.localizedDescription, contents: [.documentScanner])) + register(OCLicenseProduct(identifier: .singleShortcuts, name: shortcutsFeature.localizedName!, description: shortcutsFeature.localizedDescription, contents: [.shortcuts])) + + // - Subscription + register(OCLicenseProduct(identifier: .bundlePro, name: "Pro Features".localized, description: "Unlock all Pro Features.".localized, contents: [.documentScanner, .shortcuts])) + + // Set up App Store License Provider + let appStoreLicenseProvider = OCLicenseAppStoreProvider(items: [ + OCLicenseAppStoreItem.nonConsumableIAP(withAppStoreIdentifier: "single.documentscanner", productIdentifier: .singleDocumentScanner), + OCLicenseAppStoreItem.nonConsumableIAP(withAppStoreIdentifier: "single.shortcuts", productIdentifier: .singleShortcuts), + OCLicenseAppStoreItem.subscription(withAppStoreIdentifier: "bundle.pro", productIdentifier: .bundlePro, trialDuration: OCLicenseDuration(unit: .day, length: 14)) + ]) + + add(appStoreLicenseProvider) + + // Set up Enterprise Provider + let enterpriseProvider = OCLicenseEnterpriseProvider(unlockedProductIdentifiers: [.bundlePro]) + + add(enterpriseProvider) + } +} diff --git a/ownCloudAppShared/Intent/GetAccountIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift index fb39d5518..8932ba8a9 100644 --- a/ownCloudAppShared/Intent/GetAccountIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetAccountIntentHandler.swift @@ -25,12 +25,10 @@ public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { public func handle(intent: GetAccountIntent, completion: @escaping (GetAccountIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(GetAccountIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -42,12 +40,17 @@ public class GetAccountIntentHandler: NSObject, GetAccountIntentHandling { return } - guard let accountBookmark = OCBookmarkManager.shared.accountBookmark(for: uuid) else { + guard let (bookmark, account) = OCBookmarkManager.shared.accountBookmark(for: uuid) else { completion(GetAccountIntentResponse(code: .accountFailure, userActivity: nil)) return } - completion(GetAccountIntentResponse.success(account: accountBookmark)) + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + + completion(GetAccountIntentResponse.success(account: account)) } public func resolveAccountUUID(for intent: GetAccountIntent, with completion: @escaping (INStringResolutionResult) -> Void) { diff --git a/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift index b0ecdde50..3dd266739 100644 --- a/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetAccountsIntentHandler.swift @@ -25,9 +25,10 @@ public class GetAccountsIntentHandler: NSObject, GetAccountsIntentHandling { public func handle(intent: GetAccountsIntent, completion: @escaping (GetAccountsIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(GetAccountsIntentResponse(code: .disabled, userActivity: nil)) + return + } // if enabled, but not a valid license //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) diff --git a/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift index dee1e2731..29dc8162f 100644 --- a/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetDirectoryListingIntentHandler.swift @@ -60,12 +60,10 @@ public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingInte public func handle(intent: GetDirectoryListingIntent, completion: @escaping (GetDirectoryListingIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(GetDirectoryListingIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(GetDirectoryListingIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -81,6 +79,12 @@ public class GetDirectoryListingIntentHandler: NSObject, GetDirectoryListingInte completion(GetDirectoryListingIntentResponse(code: .accountFailure, userActivity: nil)) return } + + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(GetDirectoryListingIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + self.completion = completion OCCoreManager.shared.requestCore(for: bookmark, setup: nil, completionHandler: { (core, error) in diff --git a/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift index 8137808af..f9bc9ae63 100644 --- a/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetFileInfoIntentHandler.swift @@ -25,12 +25,10 @@ public class GetFileInfoIntentHandler: NSObject, GetFileInfoIntentHandling { public func handle(intent: GetFileInfoIntent, completion: @escaping (GetFileInfoIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(GetFileInfoIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(GetFileInfoIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -47,6 +45,11 @@ public class GetFileInfoIntentHandler: NSObject, GetFileInfoIntentHandling { return } + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(GetFileInfoIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in if error == nil, let targetItem = item { let fileInfo = FileInfo(identifier: targetItem.localID, display: targetItem.name ?? "") diff --git a/ownCloudAppShared/Intent/GetFileIntentHandler.swift b/ownCloudAppShared/Intent/GetFileIntentHandler.swift index cc9bd814a..c19b0efed 100644 --- a/ownCloudAppShared/Intent/GetFileIntentHandler.swift +++ b/ownCloudAppShared/Intent/GetFileIntentHandler.swift @@ -28,13 +28,11 @@ public class GetFileIntentHandler: NSObject, GetFileIntentHandling { public func handle(intent: GetFileIntent, completion: @escaping (GetFileIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(GetFileIntentResponse(code: .disabled, userActivity: nil)) + return + } - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) - guard !AppLockHelper().isPassCodeEnabled else { completion(GetFileIntentResponse(code: .authenticationRequired, userActivity: nil)) return @@ -50,6 +48,11 @@ public class GetFileIntentHandler: NSObject, GetFileIntentHandling { return } + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(GetFileIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in if error == nil, let item = item { if core?.localCopy(of: item) == nil { diff --git a/ownCloudAppShared/Intent/IntentSettings.swift b/ownCloudAppShared/Intent/IntentSettings.swift new file mode 100644 index 000000000..8f1ba1a7d --- /dev/null +++ b/ownCloudAppShared/Intent/IntentSettings.swift @@ -0,0 +1,90 @@ +// +// IntentSettings.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 13.01.20. +// Copyright © 2020 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2020, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import ownCloudApp +import ownCloudSDK + +class IntentSettings: NSObject { + static let shared = { + IntentSettings() + }() + + override init() { + OCLicenseManager.shared.setupLicenseManagement() + } + + var isEnabled : Bool { + return (self.classSetting(forOCClassSettingsKey: .shortcutsEnabled) as? Bool) ?? true + } + + @discardableResult + func isLicensedFor(bookmark: OCBookmark, core: OCCore? = nil, completion: ((Bool) -> Void)? = nil) -> Bool { + var environment : OCLicenseEnvironment? = core?.licenseEnvironment + + if environment == nil { + environment = OCLicenseEnvironment(bookmark: bookmark) + } + + if let environment = environment { + if completion != nil { + OCLicenseManager.shared.perform(afterCurrentlyPendingRefreshes: { + completion?(OCLicenseManager.shared.authorizationStatus(forFeature: .shortcuts, in: environment) == .granted) + }) + } else { + // Take a shortcut (ha!) if the authorization status is granted + if OCLicenseManager.shared.authorizationStatus(forFeature: .shortcuts, in: environment) == .granted { + return true + } + + // Make sure that pending refreshes have been carried out otherwise, so the result is actually conclusive + let waitGroup = DispatchGroup() + + waitGroup.enter() + + OCLicenseManager.shared.perform(afterCurrentlyPendingRefreshes: { + waitGroup.leave() + }) + + _ = waitGroup.wait(timeout: .now() + 3) + + return (OCLicenseManager.shared.authorizationStatus(forFeature: .shortcuts, in: environment) == .granted) + } + } + + return false + } +} + +// MARK: - OCClassSettings support +extension OCClassSettingsIdentifier { + static let shortcuts = OCClassSettingsIdentifier("shortcuts") +} + +extension OCClassSettingsKey { + static let shortcutsEnabled = OCClassSettingsKey("enabled") +} + +extension IntentSettings: OCClassSettingsSupport { + static var classSettingsIdentifier: OCClassSettingsIdentifier = .shortcuts + + static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? { + return [ + .shortcutsEnabled : true + ] + } +} diff --git a/ownCloudAppShared/Intent/PathExistsIntentHandler.swift b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift index 1742d22ef..da4700549 100644 --- a/ownCloudAppShared/Intent/PathExistsIntentHandler.swift +++ b/ownCloudAppShared/Intent/PathExistsIntentHandler.swift @@ -25,12 +25,10 @@ public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { public func handle(intent: PathExistsIntent, completion: @escaping (PathExistsIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(PathExistsIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(PathExistsIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -47,6 +45,11 @@ public class PathExistsIntentHandler: NSObject, PathExistsIntentHandling { return } + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(PathExistsIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + OCItemTracker().item(for: bookmark, at: path) { (error, core, item) in if error == nil, item != nil { completion(PathExistsIntentResponse.success(pathExists: true)) diff --git a/ownCloudAppShared/Intent/SaveFileIntentHandler.swift b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift index ddc6da7af..2f5529fbd 100644 --- a/ownCloudAppShared/Intent/SaveFileIntentHandler.swift +++ b/ownCloudAppShared/Intent/SaveFileIntentHandler.swift @@ -25,12 +25,10 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { public func handle(intent: SaveFileIntent, completion: @escaping (SaveFileIntentResponse) -> Void) { - // Todo: - // if Shortcuts not enabled - //completion(GetAccountIntentResponse(code: .disabled, userActivity: nil)) - - // if enabled, but not a valid license - //completion(GetAccountIntentResponse(code: .unlicensed, userActivity: nil)) + guard IntentSettings.shared.isEnabled else { + completion(SaveFileIntentResponse(code: .disabled, userActivity: nil)) + return + } guard !AppLockHelper().isPassCodeEnabled else { completion(SaveFileIntentResponse(code: .authenticationRequired, userActivity: nil)) @@ -47,6 +45,11 @@ public class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { return } + guard IntentSettings.shared.isLicensedFor(bookmark: bookmark) else { + completion(SaveFileIntentResponse(code: .unlicensed, userActivity: nil)) + return + } + var newFilename = file.filename if let filename = intent.filename as NSString?, filename.length > 0, let defaultFilename = file.filename as NSString? { var pathExtention = defaultFilename.pathExtension diff --git a/ownCloudAppShared/Tools/AppLockHelper.swift b/ownCloudAppShared/Tools/AppLockHelper.swift index b1ecd0c07..6324d7ef8 100644 --- a/ownCloudAppShared/Tools/AppLockHelper.swift +++ b/ownCloudAppShared/Tools/AppLockHelper.swift @@ -22,14 +22,12 @@ import ownCloudSDK class AppLockHelper: NSObject { var isPassCodeEnabled : Bool { - get { - let defaults = OCAppIdentity.shared.userDefaults - if let applockEnabled = defaults?.bool(forKey: "applock-lock-enabled") { - return applockEnabled - } + let defaults = OCAppIdentity.shared.userDefaults - return false + if let applockEnabled = defaults?.bool(forKey: "applock-lock-enabled") { + return applockEnabled } - } + return false + } }