storekit2 in app purchase implementation
This commit is contained in:
1 parent
008beb9444
commit
ab1d6bfa90
9 files changed
+744
-34
No files matched your search
@@ -18,6 +18,8 @@
|
|||||||
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */; };
|
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */; };
|
||||||
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
|
||||||
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
|
||||||
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 18780CC62F10D66000B90483 /* StoreKit.framework */; };
|
||||||
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18780CC92F10D6CC00B90483 /* IAPManager.swift */; };
|
||||||
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
|
||||||
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
|
||||||
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
|
||||||
@@ -179,6 +181,8 @@
|
|||||||
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouchUtil.swift; sourceTree = "<group>"; };
|
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouchUtil.swift; sourceTree = "<group>"; };
|
||||||
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
|
||||||
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
|
||||||
|
18780CC62F10D66000B90483 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||||
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = "<group>"; };
|
||||||
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
|
||||||
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
|
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
|
||||||
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
@@ -433,6 +437,7 @@
|
|||||||
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
|
||||||
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
|
||||||
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
||||||
|
18780CC72F10D66000B90483 /* StoreKit.framework in Frameworks */,
|
||||||
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
||||||
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
|
||||||
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */,
|
182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */,
|
||||||
@@ -600,6 +605,7 @@
|
|||||||
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
18780CC62F10D66000B90483 /* StoreKit.framework */,
|
||||||
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
98882A032AF60F5300C5A11C /* libavcodec.a */,
|
||||||
98882A052AF60F5300C5A11C /* libavformat.a */,
|
98882A052AF60F5300C5A11C /* libavformat.a */,
|
||||||
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
|
||||||
@@ -782,6 +788,7 @@
|
|||||||
children = (
|
children = (
|
||||||
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
|
18729EDD2E8BD68800101194 /* UUIDHelper.swift */,
|
||||||
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
|
18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */,
|
||||||
|
18780CC92F10D6CC00B90483 /* IAPManager.swift */,
|
||||||
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
97E20DD42E2164A9000D5DCA /* BandwidthTracker.h */,
|
||||||
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
97E20DD52E2164A9000D5DCA /* BandwidthTracker.m */,
|
||||||
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
97E20DD62E2164A9000D5DCA /* FloatBuffer.h */,
|
||||||
@@ -1142,6 +1149,7 @@
|
|||||||
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
|
||||||
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
|
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
|
||||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
||||||
|
18780CCA2F10D6CC00B90483 /* IAPManager.swift in Sources */,
|
||||||
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
|
||||||
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
|
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
|
||||||
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="EVd-wq-ego">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="EVd-wq-ego">
|
||||||
<device id="ipad11_0rounded" orientation="portrait" layout="fullscreen" appearance="light"/>
|
<device id="ipad11_0rounded" orientation="portrait" layout="fullscreen" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24405"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
|
||||||
<capability name="Image references" minToolsVersion="12.0"/>
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
@@ -1899,7 +1899,7 @@
|
|||||||
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
|
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
|
||||||
<segments>
|
<segments>
|
||||||
<segment title="Off"/>
|
<segment title="Off"/>
|
||||||
<segment title="Manual"/>
|
<segment title="Manual (240Hz)"/>
|
||||||
</segments>
|
</segments>
|
||||||
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
|
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ import UIKit
|
|||||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
let oscProfile = oscProfileMan.getSelectedProfile()
|
let oscProfile = oscProfileMan.getSelectedProfile()
|
||||||
pressureCurveEnabled = oscProfile.pressureCurveEnabled
|
pressureCurveEnabled = oscProfile.pressureCurveEnabled
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
self.pressureCurveEnabled = self.pressureCurveEnabled && info.valid
|
||||||
|
self.pencilTickEnabled = self.pencilTickEnabled && info.valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfile.pressureCurvePoints)
|
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfile.pressureCurvePoints)
|
||||||
let curve = PressureCurve()
|
let curve = PressureCurve()
|
||||||
curve.polylinePoints = persistedCurvePoints
|
curve.polylinePoints = persistedCurvePoints
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ final class PressureCurveView: UIView {
|
|||||||
|
|
||||||
private func drawCurve(_ ctx: CGContext) {
|
private func drawCurve(_ ctx: CGContext) {
|
||||||
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
|
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
|
||||||
ctx.setLineWidth(3)
|
ctx.setLineWidth(5)
|
||||||
let points = curve.sampleCurve(stepsPerSegment: 60)
|
let points = curve.sampleCurve(stepsPerSegment: 60)
|
||||||
for (i,p) in points.enumerated() {
|
for (i,p) in points.enumerated() {
|
||||||
let mapped = map(p)
|
let mapped = map(p)
|
||||||
@@ -300,7 +300,7 @@ final class PressureCurveView: UIView {
|
|||||||
private func drawTangents(_ ctx: CGContext) {
|
private func drawTangents(_ ctx: CGContext) {
|
||||||
let pts = curve.tangentPoints
|
let pts = curve.tangentPoints
|
||||||
ctx.setStrokeColor(UIColor.systemPurple.cgColor)
|
ctx.setStrokeColor(UIColor.systemPurple.cgColor)
|
||||||
ctx.setLineWidth(1.5)
|
ctx.setLineWidth(2.5)
|
||||||
for i in 0..<pts.count {
|
for i in 0..<pts.count {
|
||||||
let p = map(pts[i])
|
let p = map(pts[i])
|
||||||
if i==0 { ctx.move(to:p) } else { ctx.addLine(to:p) }
|
if i==0 { ctx.move(to:p) } else { ctx.addLine(to:p) }
|
||||||
@@ -374,15 +374,12 @@ final class PressureCurveView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
|
||||||
|
|
||||||
if testStage == .drawingStage {
|
if testStage == .drawingStage {
|
||||||
let altitude = touches.first!.altitudeAngle
|
let altitude = touches.first!.altitudeAngle
|
||||||
let force = (touches.first!.force/touches.first!.maximumPossibleForce)/sin(altitude)
|
let force = (touches.first!.force/touches.first!.maximumPossibleForce)/sin(altitude)
|
||||||
pressures.append(force)
|
pressures.append(force)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let touch = touches.first,
|
let touch = touches.first,
|
||||||
@@ -519,6 +516,19 @@ class PressureCurveViewController: UIViewController {
|
|||||||
curveView.widthAnchor.constraint(equalToConstant: curveWidth),
|
curveView.widthAnchor.constraint(equalToConstant: curveWidth),
|
||||||
curveView.heightAnchor.constraint(equalToConstant: curveHeight)
|
curveView.heightAnchor.constraint(equalToConstant: curveHeight)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(pencilProPurchaseAborted(_:)),
|
||||||
|
name: AddOnProduct.PencilProPack.purchaseAbortedNotification(),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(persistCurve),
|
||||||
|
name: AddOnProduct.PencilProPack.purchaseSucceededNotification(),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
@@ -547,6 +557,10 @@ class PressureCurveViewController: UIViewController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
private func loadPersistedCurve() {
|
private func loadPersistedCurve() {
|
||||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfileMan.getSelectedProfile().pressureCurvePoints)
|
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfileMan.getSelectedProfile().pressureCurvePoints)
|
||||||
@@ -618,7 +632,16 @@ class PressureCurveViewController: UIViewController {
|
|||||||
|
|
||||||
@objc private func resetTapped() {
|
@objc private func resetTapped() {
|
||||||
curveView.curve.polylinePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 1)]
|
curveView.curve.polylinePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 1)]
|
||||||
curveView.setNeedsDisplay()
|
DispatchQueue.main.async {
|
||||||
|
self.curveView.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pencilProPurchaseAborted(_ notification: Notification) {
|
||||||
|
guard let interruption = notification.object as? PurchaseInterruption else { return }
|
||||||
|
if interruption != .learnMore {
|
||||||
|
resetTapped()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func pressureRangeTest() {
|
@objc private func pressureRangeTest() {
|
||||||
@@ -636,25 +659,41 @@ class PressureCurveViewController: UIViewController {
|
|||||||
self.setupNavigationBar()
|
self.setupNavigationBar()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func saveTapped() {
|
@objc private func persistCurve() {
|
||||||
|
|
||||||
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
|
||||||
let selectedProfile = oscProfileMan.getSelectedProfile()
|
let selectedProfile = oscProfileMan.getSelectedProfile()
|
||||||
selectedProfile.pressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
|
selectedProfile.pressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
|
||||||
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
|
||||||
|
|
||||||
AlertControllerUtil.showAlert(
|
DispatchQueue.main.async {
|
||||||
in: self,
|
AlertControllerUtil.showAlert(
|
||||||
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
in: self,
|
||||||
message: SwiftLocalizationHelper.localizedString(forKey:"Pressure curve saved with current on-screen widget profile!"),
|
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
|
||||||
withCancel: false,
|
message: SwiftLocalizationHelper.localizedString(forKey:"Pressure curve saved with current on-screen widget profile!"),
|
||||||
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
|
withCancel: false,
|
||||||
countdown: 0,
|
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
|
||||||
completion: {
|
countdown: 0,
|
||||||
PencilHandler.sharedInstance?.setupPressureLUT()
|
completion: {
|
||||||
|
PencilHandler.sharedInstance?.setupPressureLUT()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func saveTapped() {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
IAPManager.checkPurchaseInfo(.PencilProPack) { info in
|
||||||
|
if info.valid {
|
||||||
|
self.persistCurve()
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
IAPManager.inAppPurchaseAction(viewController: self, product: .PencilProPack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func exitTapped() {
|
@objc private func exitTapped() {
|
||||||
|
|||||||
@@ -3085,22 +3085,28 @@
|
|||||||
"Host information updated" : {
|
"Host information updated" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "The address has been added to the host. You won’t need to enter it manually next time."
|
||||||
|
}
|
||||||
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "主机信息已更新"
|
"value" : "已为主机添加地址,下次无须手动输入该地址。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "主機資訊已更新"
|
"value" : "已為主機新增位址,下次無須手動輸入該位址。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-HK" : {
|
"zh-HK" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "主機資訊已更新"
|
"value" : "已為主機新增位址,下次無須手動輸入該位址。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3381,6 +3387,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Learn More" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "了解更多"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "了解更多"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-HK" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "了解更多"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"left" : {
|
"left" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4102,6 +4131,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"No purchase found" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "No purchase found for [%@]. If you’ve already purchased it, please try Restore Purchase."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "未找到「%@」的购买记录。如果您此前已购买,请尝试「恢复购买」。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "未偵測到「%@」的購買紀錄。若您先前已購買,請嘗試「回復購買」。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-HK" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "未偵測到「%@」的購買紀錄。若您先前已購買,請嘗試「回復購買」。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"No video received from host." : {
|
"No video received from host." : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4741,6 +4799,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Pencil Pro Pack" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Pencil 专业套装"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Pencil 專業套裝"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-HK" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Pencil 專業套裝"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PencilProPackDescription" : {
|
||||||
|
"extractionState" : "manual"
|
||||||
|
},
|
||||||
|
"PencilProPackURL" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/vrwqg8xco75n9c9z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/sl96pxao4a0oid1x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/sl96pxao4a0oid1x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-HK" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/sl96pxao4a0oid1x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"PerfModeTip" : {
|
"PerfModeTip" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -5917,6 +6030,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Restore Purchase" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "恢复购买"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "回復購買"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-HK" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "回復購買"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Resume App" : {
|
"Resume App" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -7248,6 +7384,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Unlock Now" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "即刻解锁"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "即刻解鎖"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-HK" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "即刻解鎖"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"unlockDisplayOrientationStackTip" : {
|
"unlockDisplayOrientationStackTip" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -0,0 +1,450 @@
|
|||||||
|
//
|
||||||
|
// IAPManager.swift
|
||||||
|
// VoidLink
|
||||||
|
//
|
||||||
|
// Created by True砖家 on 2026/1/9.
|
||||||
|
// Copyright © 2026 True砖家@Bilibili. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
|
@objc public enum PurchaseStatus: Int {
|
||||||
|
case notPurchased
|
||||||
|
case purchased
|
||||||
|
case revoked
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public enum PurchaseInterruption: Int {
|
||||||
|
case unlockNow
|
||||||
|
case restore
|
||||||
|
case learnMore
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public class PurchaseInfo: NSObject {
|
||||||
|
@objc public let valid: Bool
|
||||||
|
@objc public let status: PurchaseStatus
|
||||||
|
@objc public let expirationDate: NSDate?
|
||||||
|
|
||||||
|
@objc public init(status: PurchaseStatus, expirationDate: NSDate?) {
|
||||||
|
self.status = status
|
||||||
|
self.expirationDate = expirationDate
|
||||||
|
self.valid = (status == .purchased)
|
||||||
|
&& (expirationDate?.timeIntervalSinceNow ?? .infinity > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public enum AddOnProduct: UInt8, CaseIterable {
|
||||||
|
case PencilProPack
|
||||||
|
|
||||||
|
func productId() -> String {
|
||||||
|
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
||||||
|
switch (self, bundleId) {
|
||||||
|
case (.PencilProPack, "com.voidlink.iOS"):
|
||||||
|
return "com.pencilpro.voidlink.iOS"
|
||||||
|
case (.PencilProPack, "com.voidlinkextreme.iOS"):
|
||||||
|
return "com.pencilpro.voidlinkextreme.iOS"
|
||||||
|
case (.PencilProPack, "com.voidlink.tf.debug10.iOS"):
|
||||||
|
return "com.pencilpro.voidlink.debug.iOS"
|
||||||
|
case (.PencilProPack, "com.voidlink.tf.iOS"):
|
||||||
|
return "com.pencilpro.voidlink.tf.iOS"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productName() -> String {
|
||||||
|
switch self {
|
||||||
|
case .PencilProPack:
|
||||||
|
return SwiftLocalizationHelper.localizedString(forKey: "Pencil Pro Pack")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productURL() -> String {
|
||||||
|
switch self {
|
||||||
|
case .PencilProPack:
|
||||||
|
return SwiftLocalizationHelper.localizedString(forKey: "PencilProPackURL")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func productDescription() -> String {
|
||||||
|
switch self {
|
||||||
|
case .PencilProPack:
|
||||||
|
return SwiftLocalizationHelper.localizedString(forKey: "PencilProPackDescription")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func purchaseAbortedNotification() -> Notification.Name {
|
||||||
|
switch self {
|
||||||
|
case .PencilProPack:
|
||||||
|
return Notification.Name("PencilProPurchaseAbortedNotification")
|
||||||
|
default:
|
||||||
|
return Notification.Name("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func purchaseSucceededNotification() -> Notification.Name {
|
||||||
|
switch self {
|
||||||
|
case .PencilProPack:
|
||||||
|
return Notification.Name("PencilProPurchaseSucceededNotification")
|
||||||
|
default:
|
||||||
|
return Notification.Name("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from(productId: String) -> AddOnProduct? {
|
||||||
|
switch productId {
|
||||||
|
case "com.pencilpro.voidlink.iOS",
|
||||||
|
"com.pencilpro.voidlinkextreme.iOS",
|
||||||
|
"com.pencilpro.voidlink.debug.iOS",
|
||||||
|
"com.pencilpro.voidlink.tf.iOS":
|
||||||
|
return .PencilProPack
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public protocol IAPManagerDelegate: NSObjectProtocol {
|
||||||
|
@objc func iapManagerDidFetchProducts(_ products: [String])
|
||||||
|
|
||||||
|
@objc func iapManagerDidFailWithError(_ error: Error)
|
||||||
|
|
||||||
|
@objc func iapManagerDidPurchase(_ product: AddOnProduct)
|
||||||
|
|
||||||
|
@objc func iapManagerDidRestore(_ product: AddOnProduct)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public class IAPManager: NSObject {
|
||||||
|
|
||||||
|
@objc public static let shared = IAPManager()
|
||||||
|
@objc public weak var delegate: IAPManagerDelegate?
|
||||||
|
@objc public private(set) var fetchedProductIds: [String] = []
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
if #available(iOS 15.0, *, *) {
|
||||||
|
Task {
|
||||||
|
await listenForTransactions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Fetch Products (StoreKit 2)
|
||||||
|
|
||||||
|
@objc public func fetchProducts() {
|
||||||
|
if #available(iOS 15.0, *, *) {
|
||||||
|
Task {
|
||||||
|
await fetchProductsStoreKit2()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fetchProductsLegacy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *, *)
|
||||||
|
private func fetchProductsStoreKit2() async {
|
||||||
|
let ids = Set(AddOnProduct.allCases.map { $0.productId() })
|
||||||
|
do {
|
||||||
|
let sk2Products = try await Product.products(for: ids)
|
||||||
|
let idsFetched = sk2Products.map { $0.id }
|
||||||
|
await MainActor.run {
|
||||||
|
self.delegate?.iapManagerDidFetchProducts(idsFetched)
|
||||||
|
self.fetchedProductIds = idsFetched
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
self.delegate?.iapManagerDidFailWithError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public var skProducts: [SKProduct] = []
|
||||||
|
private var productsRequest: SKProductsRequest?
|
||||||
|
private func fetchProductsLegacy() {
|
||||||
|
let productIds = Set(AddOnProduct.allCases.map { $0.productId() })
|
||||||
|
|
||||||
|
|
||||||
|
guard productsRequest == nil else { return }
|
||||||
|
|
||||||
|
guard productIds.count>0 else { return }
|
||||||
|
|
||||||
|
fetchedProductIds = Array(productIds)
|
||||||
|
productsRequest = SKProductsRequest(
|
||||||
|
productIdentifiers: productIds
|
||||||
|
)
|
||||||
|
productsRequest?.delegate = self
|
||||||
|
productsRequest?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Purchase (StoreKit 2)
|
||||||
|
|
||||||
|
@objc public func purchase(_ product: AddOnProduct) {
|
||||||
|
if #available(iOS 15.0, *, *) {
|
||||||
|
Task {
|
||||||
|
await purchaseStoreKit2(product)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
purchaseLegacy(product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *, *)
|
||||||
|
private func purchaseStoreKit2(_ product: AddOnProduct) async {
|
||||||
|
let pid = product.productId()
|
||||||
|
do {
|
||||||
|
let products = try await Product.products(for: [pid])
|
||||||
|
guard let sk2p = products.first else {
|
||||||
|
throw NSError(
|
||||||
|
domain: "IAPManager",
|
||||||
|
code: -2,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: "Product not found"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try await sk2p.purchase()
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let verification):
|
||||||
|
switch verification {
|
||||||
|
case .verified(let transaction):
|
||||||
|
await MainActor.run {
|
||||||
|
self.delegate?.iapManagerDidPurchase(product)
|
||||||
|
}
|
||||||
|
await transaction.finish()
|
||||||
|
NotificationCenter.default.post(name: product.purchaseSucceededNotification(), object: nil)
|
||||||
|
case .unverified(_, let err):
|
||||||
|
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow)
|
||||||
|
await MainActor.run {
|
||||||
|
self.delegate?.iapManagerDidFailWithError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .userCancelled:
|
||||||
|
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await MainActor.run {
|
||||||
|
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.unlockNow)
|
||||||
|
self.delegate?.iapManagerDidFailWithError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func purchaseLegacy(_ product: AddOnProduct) {
|
||||||
|
let productId = product.productId()
|
||||||
|
print("products \(skProducts.count)")
|
||||||
|
guard let skProduct = skProducts.first(
|
||||||
|
where: {
|
||||||
|
print("$0.productIdentifier \($0.productIdentifier), productId \(productId)")
|
||||||
|
return $0.productIdentifier == productId
|
||||||
|
}
|
||||||
|
) else { return }
|
||||||
|
let payment = SKPayment(product: skProduct)
|
||||||
|
SKPaymentQueue.default().add(payment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Restore (StoreKit 2)
|
||||||
|
|
||||||
|
@objc public func restorePurchasesStoreKit2() {
|
||||||
|
if #available(iOS 15.0, *, *) {
|
||||||
|
Task {
|
||||||
|
await restoreStoreKit2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
restorePurchasesLegacy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *, *)
|
||||||
|
private func restoreStoreKit2() async {
|
||||||
|
for await result in Transaction.currentEntitlements {
|
||||||
|
if case .verified(let transaction) = result,
|
||||||
|
let adp = AddOnProduct.from(productId: transaction.productID) {
|
||||||
|
await MainActor.run {
|
||||||
|
self.delegate?.iapManagerDidRestore(adp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restorePurchasesLegacy() {
|
||||||
|
SKPaymentQueue.default().restoreCompletedTransactions()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Listen update
|
||||||
|
|
||||||
|
@available(iOS 15.0, *, *)
|
||||||
|
private func listenForTransactions() async {
|
||||||
|
for await verificationResult in Transaction.updates {
|
||||||
|
if case .verified(let transaction) = verificationResult,
|
||||||
|
let adp = AddOnProduct.from(productId: transaction.productID) {
|
||||||
|
await MainActor.run {
|
||||||
|
// 这里把支付成功回调出来
|
||||||
|
self.delegate?.iapManagerDidPurchase(adp)
|
||||||
|
}
|
||||||
|
await transaction.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Utils
|
||||||
|
@available(iOS 15.0, *)
|
||||||
|
@objc static public func checkPurchaseInfo(
|
||||||
|
_ product: AddOnProduct,
|
||||||
|
completion: @escaping (PurchaseInfo) -> Void
|
||||||
|
) {
|
||||||
|
let productID = product.productId()
|
||||||
|
Task {
|
||||||
|
var status: PurchaseStatus = .notPurchased
|
||||||
|
var expiration: Date? = nil
|
||||||
|
|
||||||
|
for await verificationResult in Transaction.currentEntitlements {
|
||||||
|
if case .verified(let transaction) = verificationResult,
|
||||||
|
transaction.productID == productID {
|
||||||
|
|
||||||
|
// 有退款 / 撤销
|
||||||
|
if transaction.revocationDate != nil {
|
||||||
|
status = .revoked
|
||||||
|
} else {
|
||||||
|
status = .purchased
|
||||||
|
expiration = transaction.expirationDate
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = PurchaseInfo(status: status, expirationDate: expiration as NSDate?)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
completion(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *)
|
||||||
|
static public func checkPurchaseInfo(
|
||||||
|
_ product: AddOnProduct
|
||||||
|
) async -> PurchaseInfo {
|
||||||
|
await withCheckedContinuation { continuation in
|
||||||
|
checkPurchaseInfo(product) { info in
|
||||||
|
continuation.resume(returning: info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static public func inAppPurchaseAction(viewController: UIViewController, product: AddOnProduct){
|
||||||
|
|
||||||
|
let alert = UIAlertController(title: product.productName(),
|
||||||
|
message: SwiftLocalizationHelper.localizedString(forKey: "No purchase found", product.productName()),
|
||||||
|
preferredStyle: .alert)
|
||||||
|
|
||||||
|
let unlockAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Unlock Now"), style: .default) { _ in
|
||||||
|
IAPManager.shared.purchase(product)
|
||||||
|
}
|
||||||
|
|
||||||
|
let restoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Restore Purchase"), style: .default) { _ in
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
Task{
|
||||||
|
do {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
try await AppStore.sync()
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.restore)
|
||||||
|
print("Restore failed: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in
|
||||||
|
NotificationCenter.default.post(name: product.purchaseAbortedNotification(), object: PurchaseInterruption.learnMore)
|
||||||
|
if let url = URL(string: product.productURL()) {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.addAction(unlockAction)
|
||||||
|
alert.addAction(restoreAction)
|
||||||
|
alert.addAction(learnMoreAction)
|
||||||
|
|
||||||
|
viewController.present(alert, animated: true, completion: {
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc static public func inAppPurchaseURLMessage(viewController: UIViewController, product: AddOnProduct){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension IAPManager: SKProductsRequestDelegate {
|
||||||
|
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
|
||||||
|
skProducts = response.products
|
||||||
|
let productIds = Array(skProducts.map { $0.productIdentifier })
|
||||||
|
delegate?.iapManagerDidFetchProducts(productIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func request(_ request: SKRequest, didFailWithError error: Error) {
|
||||||
|
delegate?.iapManagerDidFailWithError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IAPManager: SKPaymentTransactionObserver {
|
||||||
|
|
||||||
|
public func paymentQueue(_ queue: SKPaymentQueue,
|
||||||
|
updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||||
|
|
||||||
|
print("paymentQueue \(CACurrentMediaTime())")
|
||||||
|
|
||||||
|
for transaction in transactions {
|
||||||
|
let productId = transaction.payment.productIdentifier
|
||||||
|
guard let product = AddOnProduct.from(productId: productId) else {
|
||||||
|
SKPaymentQueue.default().finishTransaction(transaction)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
print("transaction.transactionState \(transaction.transactionState)")
|
||||||
|
|
||||||
|
switch transaction.transactionState {
|
||||||
|
|
||||||
|
case .purchased:
|
||||||
|
delegate?.iapManagerDidPurchase(product)
|
||||||
|
SKPaymentQueue.default().finishTransaction(transaction)
|
||||||
|
|
||||||
|
case .restored:
|
||||||
|
delegate?.iapManagerDidRestore(product)
|
||||||
|
SKPaymentQueue.default().finishTransaction(transaction)
|
||||||
|
|
||||||
|
case .failed:
|
||||||
|
if let error = transaction.error {
|
||||||
|
delegate?.iapManagerDidFailWithError(error)
|
||||||
|
}
|
||||||
|
SKPaymentQueue.default().finishTransaction(transaction)
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#import "ThemeManager.h"
|
#import "ThemeManager.h"
|
||||||
#import "FixedTintImageView.h"
|
#import "FixedTintImageView.h"
|
||||||
#import "VoidLink-Swift.h" // not used yet.
|
#import "VoidLink-Swift.h"
|
||||||
|
|
||||||
#if !TARGET_OS_TV
|
#if !TARGET_OS_TV
|
||||||
#import "SettingsViewController.h"
|
#import "SettingsViewController.h"
|
||||||
@@ -1683,6 +1683,30 @@ static NSMutableSet* hostList;
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
[self prewarmSoftKeyboard];
|
[self prewarmSoftKeyboard];
|
||||||
|
|
||||||
|
[IAPManager.shared fetchProducts];
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (@available(iOS 15.0, *)) {
|
||||||
|
[IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) {
|
||||||
|
switch (info.status) {
|
||||||
|
case PurchaseStatusPurchased:
|
||||||
|
NSLog(@"PurchaseStatus Purchased");
|
||||||
|
break;
|
||||||
|
case PurchaseStatusNotPurchased:
|
||||||
|
NSLog(@"PurchaseStatus NotPurchased");
|
||||||
|
break;
|
||||||
|
case PurchaseStatusRevoked:
|
||||||
|
NSLog(@"PurchaseStatus Revoked");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
NSLog(@"PurchaseStatus Valid: %d", info.valid);
|
||||||
|
NSLog(@"PurchaseStatus Expiration: %@", info.expirationDate);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)prewarmSoftKeyboard {
|
- (void)prewarmSoftKeyboard {
|
||||||
|
|||||||
@@ -467,6 +467,10 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
selector:@selector(reloadGameProfileConfigs)
|
selector:@selector(reloadGameProfileConfigs)
|
||||||
name:@"OscLayoutCloseNotification"
|
name:@"OscLayoutCloseNotification"
|
||||||
object:nil];
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(pencilProPurchaseAborted)
|
||||||
|
name:@"PencilProPurchaseAbortedNotification"
|
||||||
|
object:nil];
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||||
if(self.mainFrameViewController.settingsExpandedInStreamView){
|
if(self.mainFrameViewController.settingsExpandedInStreamView){
|
||||||
@@ -3334,6 +3338,24 @@ BOOL isCustomResolution(int resolutionSelected) {
|
|||||||
|
|
||||||
- (void)pencilTickModeChanged:(UISegmentedControl* )sender{
|
- (void)pencilTickModeChanged:(UISegmentedControl* )sender{
|
||||||
[self setHidden:sender.selectedSegmentIndex != ManualTick forStack:self.pencilTickIntervalStack];
|
[self setHidden:sender.selectedSegmentIndex != ManualTick forStack:self.pencilTickIntervalStack];
|
||||||
|
|
||||||
|
if(settingsViewJustExpanded) return;
|
||||||
|
|
||||||
|
if (@available(iOS 15.0, *)) {
|
||||||
|
if(sender.selectedSegmentIndex != ManualTick) return;
|
||||||
|
[IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) {
|
||||||
|
if(!info.valid){
|
||||||
|
[IAPManager inAppPurchaseActionWithViewController:self product:AddOnProductPencilProPack];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)pencilProPurchaseAborted{
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
self.pencilTickSelector.selectedSegmentIndex = PencilTickDisabled;
|
||||||
|
[self pencilTickModeChanged:self.pencilTickSelector];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)pencilTickIntervalSliderMoved:(UISlider* )sender{
|
- (void)pencilTickIntervalSliderMoved:(UISlider* )sender{
|
||||||
|
|||||||
@@ -1688,31 +1688,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bVv-CW-kN8.segmentTitles[1]" : {
|
"bVv-CW-kN8.segmentTitles[1]" : {
|
||||||
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[1] = \"Manual\"; ObjectID = \"bVv-CW-kN8\";",
|
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[1] = \"Manual (240Hz)\"; ObjectID = \"bVv-CW-kN8\";",
|
||||||
"extractionState" : "extracted_with_value",
|
"extractionState" : "extracted_with_value",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "new",
|
"state" : "new",
|
||||||
"value" : "Manual"
|
"value" : "Manual (240Hz)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "手动"
|
"value" : "手动 (240Hz)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hant" : {
|
"zh-Hant" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "手動"
|
"value" : "手動 (240Hz)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-HK" : {
|
"zh-HK" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "手動"
|
"value" : "手動 (240Hz)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user