Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Rules:

- Locked Word, Excel, PowerPoint, Pages, Numbers and Keynote files open with
their password.
- Lite sells a yearly pass for €3: no ads and all Pro features. The heart on
the start screen, "Support the app" in the menu and the Pro gate offer it.

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
ReferencedContainer = "container:OpenDocumentReader.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../../configs/lite/Pass.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand Down
3 changes: 3 additions & 0 deletions OpenDocumentReader/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

StoreReviewHelper.incrementAppOpenedCount()

// Lite only: asks the App Store whether the yearly pass is active
Pass.shared.start()

return true
}

Expand Down
102 changes: 93 additions & 9 deletions OpenDocumentReader/DocumentBrowserViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
A document browser view controller subclass that implements methods for creating, opening, and importing documents.
*/

import StoreKit
import UIKit

class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate {
class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate,
SKStoreProductViewControllerDelegate
{

override func viewDidLoad() {
super.viewDidLoad()
delegate = self

allowsDocumentCreation = false
allowsPickingMultipleItems = false

NotificationCenter.default.addObserver(
self, selector: #selector(passUnlocked(_:)), name: Pass.didUnlock, object: nil)
}

override func viewDidAppear(_ animated: Bool) {
Expand All @@ -30,6 +36,7 @@ class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocument

StoreReviewHelper.checkAndAskForReview()

updateBarButtons()
refreshPrivacyButton()
}

Expand Down Expand Up @@ -72,21 +79,98 @@ class DocumentBrowserViewController: UIDocumentBrowserViewController, UIDocument
}
}

// MARK: - Support

/// Shown once consent has been refreshed. It stays after the pass: withdrawal outlives the ads.
private var showsPrivacyButton = false

/// "Support the app" in Lite until the pass is active, where it is seen without a menu, and
/// the privacy options.
private func updateBarButtons() {
var items: [UIBarButtonItem] = []

if Pass.shared.offersSupport {
// a glyph: with a title, iOS 26 folds it into the overflow menu, where it is not seen
let support = UIBarButtonItem(
image: UIImage(systemName: "heart"), style: .plain, target: self,
action: #selector(showSupport(_:)))
support.accessibilityLabel = NSLocalizedString("menu_support", comment: "")
items.append(support)
}

if showsPrivacyButton {
items.append(
UIBarButtonItem(
title: NSLocalizedString("privacy", value: "Privacy", comment: ""), style: .plain, target: self,
action: #selector(showPrivacyOptions(_:))))
}

additionalTrailingNavigationBarButtonItems = items
}

@objc private func showSupport(_ sender: UIBarButtonItem) {
AnalyticsManager.shared.report("landing_support")

PassViewController.present(from: self, feature: nil) { [weak self] in
self?.openProOnAppStore()
}
}

/// The Pro app's page, over the browser.
private func openProOnAppStore() {
AnalyticsManager.shared.report(AnalyticsConstants.eventAddToCart)

let store = SKStoreProductViewController()
store.delegate = self
store.loadProduct(withParameters: [SKStoreProductParameterITunesItemIdentifier: Constants.proAppStoreId]) {
loaded, error in
if let error {
CrashManager.shared.log(error)
}

guard loaded else { return }

DispatchQueue.main.async {
self.present(store, animated: true)
}
}
}

func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
viewController.dismiss(animated: true)
}

/// The button goes. The reader says thank you if one is open; otherwise the browser does.
@objc private func passUnlocked(_ notification: Notification) {
updateBarButtons()

guard presentedViewController == nil || presentedViewController is PassViewController else { return }

let purchased = notification.userInfo?["purchased"] as? Bool ?? false
AnalyticsManager.shared.report(purchased ? "pass_unlocked" : "pass_restored")

// the sheet goes away first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in
guard let self, self.presentedViewController == nil else { return }

let alert = UIAlertController(
title: nil, message: NSLocalizedString(purchased ? "pass_unlocked" : "pass_restored", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default))

self.present(alert, animated: true)
}
}

// MARK: - Privacy

/// Refresh consent and expose privacy options in the browser toolbar.
private func refreshPrivacyButton() {
guard Features.withAds else { return }

AdPrivacy.refresh {
let item = UIBarButtonItem(
title: NSLocalizedString("privacy", value: "Privacy", comment: ""),
style: .plain,
target: self,
action: #selector(self.showPrivacyOptions(_:))
)

self.additionalTrailingNavigationBarButtonItems = [item]
self.showsPrivacyButton = true
self.updateBarButtons()
}
}

Expand Down
Loading
Loading