Skip to content

Commit

Permalink
[feature/licensing] Licensing support (#571)
Browse files Browse the repository at this point in the history
* - 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 9a0bc93.

* 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 9a0bc93.

* 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 <[email protected]>
Co-authored-by: Matthias Hühne <[email protected]>
  • Loading branch information
3 people authored Feb 7, 2020
1 parent c72308f commit 53b965e
Show file tree
Hide file tree
Showing 92 changed files with 7,174 additions and 162 deletions.
436 changes: 429 additions & 7 deletions ownCloud.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@
value = "1"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "oc:authentication-oauth2.oa2-browser-session-class"
value = "string:UIWebView"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "oc:core.override-reachability-signal"
value = "true"
Expand Down Expand Up @@ -187,7 +192,7 @@
<EnvironmentVariable
key = "oc:log.log-synchronous"
value = "true"
isEnabled = "NO">
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "oc:log.log-only-tags"
Expand Down
94 changes: 94 additions & 0 deletions ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloudApp.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1120"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DCC0855B2293F1FD008CC05C"
BuildableName = "ownCloudApp.framework"
BlueprintName = "ownCloudApp"
ReferencedContainer = "container:ownCloud.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DCC085632293F1FD008CC05C"
BuildableName = "ownCloudAppTests.xctest"
BlueprintName = "ownCloudAppTests"
ReferencedContainer = "container:ownCloud.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<EnvironmentVariables>
<EnvironmentVariable
key = "oc:log.log-level"
value = "0"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "oc:log.log-colored"
value = "true"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "oc:log.log-synchronous"
value = "true"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DCC0855B2293F1FD008CC05C"
BuildableName = "ownCloudApp.framework"
BlueprintName = "ownCloudApp"
ReferencedContainer = "container:ownCloud.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
8 changes: 6 additions & 2 deletions ownCloud/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import UIKit
import ownCloudSDK
import ownCloudApp
import ownCloudAppShared

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
59 changes: 57 additions & 2 deletions ownCloud/Client/Actions/Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import UIKit
import ownCloudSDK
import ownCloudApp

enum ActionCategory {
case normal
Expand Down Expand Up @@ -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? {
Expand All @@ -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()
})

Expand Down
3 changes: 3 additions & 0 deletions ownCloud/Client/Actions/MoreViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions ownCloud/Client/Actions/Scanner/ScanAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/

import ownCloudSDK
import ownCloudApp
import ownCloudAppShared
import VisionKit

class ScanAction: Action, VNDocumentCameraViewControllerDelegate {
Expand All @@ -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 {
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions ownCloud/Client/Viewer/DisplayHostViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -331,23 +331,23 @@ 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) {
self.setViewControllers([vc], direction: .forward, animated: false, completion: nil)
}
}
}

@objc private func handlePlayNextMedia(notification:Notification) {
if let mediaController = self.viewControllers?.first as? MediaDisplayViewController {
if let vc = vendNewViewController(from: mediaController, .after) {
self.setViewControllers([vc], direction: .forward, animated: false, completion: nil)
}
}
}

@objc private func handlePlayPreviousMedia(notification:Notification) {
if let mediaController = self.viewControllers?.first as? MediaDisplayViewController {
if let vc = vendNewViewController(from: mediaController, .before) {
Expand Down
19 changes: 9 additions & 10 deletions ownCloud/Client/Viewer/Media/MediaDisplayViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 }
Expand All @@ -328,7 +328,6 @@ class MediaDisplayViewController : DisplayViewController {
}

MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo

updateNowPlayingTimeline()
}
}
Expand Down
2 changes: 1 addition & 1 deletion ownCloud/Key Commands/KeyCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 53b965e

Please sign in to comment.