diff --git a/PVLibrary/PVLibrary.xcodeproj/project.pbxproj b/PVLibrary/PVLibrary.xcodeproj/project.pbxproj index 6759da889a..6b084502a4 100644 --- a/PVLibrary/PVLibrary.xcodeproj/project.pbxproj +++ b/PVLibrary/PVLibrary.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ B30E7AEB26E480310051DC6A /* BehaviorRelay+RangeReplaceableCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30E7AEA26E480310051DC6A /* BehaviorRelay+RangeReplaceableCollection.swift */; }; B31117AC218EB24300C495A2 /* LzmaSDKObjCReader.h in Headers */ = {isa = PBXBuildFile; fileRef = B3AF474721071CA6002211EE /* LzmaSDKObjCReader.h */; settings = {ATTRIBUTES = (Public, ); }; }; B31117AE218EB25F00C495A2 /* LzmaSDKObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = B3AF475221071CA6002211EE /* LzmaSDKObjC.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B31ACB37294DBBBD002D7E2F /* PVPatreonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31ACB36294DBBBD002D7E2F /* PVPatreonTests.swift */; }; + B31ACB38294DBBBD002D7E2F /* PVPatreon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3FAC8DB292B1DDD005E8B11 /* PVPatreon.framework */; }; + B31ACB43294DBE02002D7E2F /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8F5292B2551005E8B11 /* Keychain.swift */; }; B3274E602106BA1300857F52 /* PVAppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3274E5E2106BA1200857F52 /* PVAppConstants.swift */; }; B3274E612106BA5B00857F52 /* PVMediaCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3579D222106B4D600DDEBD6 /* PVMediaCache.swift */; }; B3274E642106BAA200857F52 /* NSString+Hashing.m in Sources */ = {isa = PBXBuildFile; fileRef = B3274E622106BAA200857F52 /* NSString+Hashing.m */; }; @@ -214,16 +217,16 @@ B3CDEEB021D493AC000C55F7 /* GameImporter2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CDEEAF21D493AC000C55F7 /* GameImporter2.swift */; }; B3CDEEB321D497CD000C55F7 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CDEEB221D497CD000C55F7 /* Package.swift */; }; B3CDEECB21D4CF61000C55F7 /* DirectoryWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CDEECA21D4CF61000C55F7 /* DirectoryWatcher.swift */; }; + B3E7ADE7294DC37900548C24 /* PatreonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E7292B1E18005E8B11 /* PatreonAPI.swift */; }; + B3E7ADE8294DC41A00548C24 /* Consts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31ACB41294DBDAC002D7E2F /* Consts.swift */; }; + B3E7ADEA294DC45F00548C24 /* PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E7ADE9294DC45F00548C24 /* PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift */; }; + B3E7ADEC294DC79000548C24 /* Benefit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E5292B1E18005E8B11 /* Benefit.swift */; }; + B3E7ADED294DC7A200548C24 /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E8292B1E1A005E8B11 /* Tier.swift */; }; + B3E7ADEF294DC83500548C24 /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E4292B1E17005E8B11 /* Campaign.swift */; }; + B3E7ADF1294DC97500548C24 /* Patron.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E3292B1E17005E8B11 /* Patron.swift */; }; + B3E7ADF2294DCACD00548C24 /* PatreonAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E6292B1E18005E8B11 /* PatreonAccount.swift */; }; B3FAC8DE292B1DDD005E8B11 /* PVPatreon.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FAC8DD292B1DDD005E8B11 /* PVPatreon.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B3FAC8E9292B1E1B005E8B11 /* Patron.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E3292B1E17005E8B11 /* Patron.swift */; }; - B3FAC8EA292B1E1B005E8B11 /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E4292B1E17005E8B11 /* Campaign.swift */; }; - B3FAC8EB292B1E1B005E8B11 /* Benefit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E5292B1E18005E8B11 /* Benefit.swift */; }; - B3FAC8EC292B1E1B005E8B11 /* PatreonAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E6292B1E18005E8B11 /* PatreonAccount.swift */; }; - B3FAC8ED292B1E1B005E8B11 /* PatreonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E7292B1E18005E8B11 /* PatreonAPI.swift */; }; - B3FAC8EE292B1E1B005E8B11 /* Tier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8E8292B1E1A005E8B11 /* Tier.swift */; }; B3FAC8EF292B1E1C005E8B11 /* PVLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3579D0F2106B11D00DDEBD6 /* PVLibrary.framework */; }; - B3FAC8F0292B1E1C005E8B11 /* PVLibrary.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3579D0F2106B11D00DDEBD6 /* PVLibrary.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B3FAC8F6292B2551005E8B11 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8F5292B2551005E8B11 /* Keychain.swift */; }; B3FAC8F9292B26EA005E8B11 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = B3FAC8F8292B26EA005E8B11 /* KeychainAccess */; }; B3FAC8FB292B3155005E8B11 /* PVUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8FA292B3155005E8B11 /* PVUser.swift */; }; B3FAC8FD292B3240005E8B11 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FAC8FC292B3240005E8B11 /* User.swift */; }; @@ -232,6 +235,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + B31ACB39294DBBBD002D7E2F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B3579D062106B11D00DDEBD6 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B3FAC8DA292B1DDD005E8B11; + remoteInfo = PVPatreon; + }; B3C04B672107867200E7AF79 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B3579D062106B11D00DDEBD6 /* Project object */; @@ -260,17 +270,6 @@ name = "Copy Framework"; runOnlyForDeploymentPostprocessing = 0; }; - B3FAC8F3292B1E1C005E8B11 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - B3FAC8F0292B1E1C005E8B11 /* PVLibrary.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -286,6 +285,11 @@ B3067F732106B9130091437F /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; B30E7AEA26E480310051DC6A /* BehaviorRelay+RangeReplaceableCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BehaviorRelay+RangeReplaceableCollection.swift"; sourceTree = ""; }; B317380127841959002D3ACD /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Build.xcconfig; path = ../Build.xcconfig; sourceTree = ""; }; + B31ACB26294DA31D002D7E2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + B31ACB27294DA388002D7E2F /* PatreonConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PatreonConfig.xcconfig; sourceTree = ""; }; + B31ACB34294DBBBD002D7E2F /* PVPatreonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PVPatreonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B31ACB36294DBBBD002D7E2F /* PVPatreonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PVPatreonTests.swift; sourceTree = ""; }; + B31ACB41294DBDAC002D7E2F /* Consts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consts.swift; sourceTree = ""; }; B324C2EC219192FF009F4EDC /* PVSupport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PVSupport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B3274E5D2106BA1200857F52 /* PVEmulatorConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PVEmulatorConfiguration.swift; sourceTree = ""; }; B3274E5E2106BA1200857F52 /* PVAppConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PVAppConstants.swift; sourceTree = ""; }; @@ -681,6 +685,7 @@ B3CDEEAF21D493AC000C55F7 /* GameImporter2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameImporter2.swift; sourceTree = ""; }; B3CDEEB221D497CD000C55F7 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; B3CDEECA21D4CF61000C55F7 /* DirectoryWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryWatcher.swift; sourceTree = ""; }; + B3E7ADE9294DC45F00548C24 /* PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift"; sourceTree = ""; }; B3FAC8DB292B1DDD005E8B11 /* PVPatreon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PVPatreon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B3FAC8DD292B1DDD005E8B11 /* PVPatreon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PVPatreon.h; sourceTree = ""; }; B3FAC8E3292B1E17005E8B11 /* Patron.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Patron.swift; path = ../../../PVPatreon/Patron.swift; sourceTree = ""; }; @@ -697,6 +702,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + B31ACB31294DBBBC002D7E2F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B31ACB38294DBBBD002D7E2F /* PVPatreon.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B3579D0C2106B11D00DDEBD6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -752,6 +765,14 @@ name = Frameworks; sourceTree = ""; }; + B31ACB35294DBBBD002D7E2F /* PVPatreonTests */ = { + isa = PBXGroup; + children = ( + B31ACB36294DBBBD002D7E2F /* PVPatreonTests.swift */, + ); + path = PVPatreonTests; + sourceTree = ""; + }; B3274E5C2106BA1200857F52 /* Configuration */ = { isa = PBXGroup; children = ( @@ -868,6 +889,7 @@ B3579D112106B11D00DDEBD6 /* PVLibrary */, B3C04B622107867200E7AF79 /* Tests */, B3FAC8DC292B1DDD005E8B11 /* PVPatreon */, + B31ACB35294DBBBD002D7E2F /* PVPatreonTests */, B3579D102106B11D00DDEBD6 /* Products */, B3067F492106B5A20091437F /* Frameworks */, B305F082276B5152003AE510 /* PVLibrary-tvOS copy-Info.plist */, @@ -880,6 +902,7 @@ B3579D0F2106B11D00DDEBD6 /* PVLibrary.framework */, B3C04B612107867200E7AF79 /* PVLibraryTests.xctest */, B3FAC8DB292B1DDD005E8B11 /* PVPatreon.framework */, + B31ACB34294DBBBD002D7E2F /* PVPatreonTests.xctest */, ); name = Products; sourceTree = ""; @@ -1485,14 +1508,18 @@ B3FAC8DC292B1DDD005E8B11 /* PVPatreon */ = { isa = PBXGroup; children = ( + B3FAC8DD292B1DDD005E8B11 /* PVPatreon.h */, + B31ACB27294DA388002D7E2F /* PatreonConfig.xcconfig */, + B31ACB26294DA31D002D7E2F /* Info.plist */, B3FAC8F4292B2551005E8B11 /* Keychain */, B3FAC8E5292B1E18005E8B11 /* Benefit.swift */, B3FAC8E4292B1E17005E8B11 /* Campaign.swift */, B3FAC8E6292B1E18005E8B11 /* PatreonAccount.swift */, B3FAC8E7292B1E18005E8B11 /* PatreonAPI.swift */, + B3E7ADE9294DC45F00548C24 /* PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift */, B3FAC8E3292B1E17005E8B11 /* Patron.swift */, B3FAC8E8292B1E1A005E8B11 /* Tier.swift */, - B3FAC8DD292B1DDD005E8B11 /* PVPatreon.h */, + B31ACB41294DBDAC002D7E2F /* Consts.swift */, ); path = PVPatreon; sourceTree = ""; @@ -1541,6 +1568,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + B31ACB33294DBBBC002D7E2F /* PVPatreonTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B31ACB3B294DBBBD002D7E2F /* Build configuration list for PBXNativeTarget "PVPatreonTests" */; + buildPhases = ( + B31ACB30294DBBBC002D7E2F /* Sources */, + B31ACB31294DBBBC002D7E2F /* Frameworks */, + B31ACB32294DBBBC002D7E2F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B31ACB3A294DBBBD002D7E2F /* PBXTargetDependency */, + ); + name = PVPatreonTests; + productName = PVPatreonTests; + productReference = B31ACB34294DBBBD002D7E2F /* PVPatreonTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; B3579D0E2106B11D00DDEBD6 /* PVLibrary */ = { isa = PBXNativeTarget; buildConfigurationList = B3579D172106B11D00DDEBD6 /* Build configuration list for PBXNativeTarget "PVLibrary" */; @@ -1593,7 +1638,6 @@ B3FAC8D7292B1DDD005E8B11 /* Sources */, B3FAC8D8292B1DDD005E8B11 /* Frameworks */, B3FAC8D9292B1DDD005E8B11 /* Resources */, - B3FAC8F3292B1E1C005E8B11 /* Embed Frameworks */, ); buildRules = ( ); @@ -1617,10 +1661,13 @@ BuildIndependentTargetsInParallel = YES; CLASSPREFIX = PV; DefaultBuildSystemTypeForWorkspace = Latest; - LastSwiftUpdateCheck = 0940; + LastSwiftUpdateCheck = 1420; LastUpgradeCheck = 1300; ORGANIZATIONNAME = "Provenance Emu"; TargetAttributes = { + B31ACB33294DBBBC002D7E2F = { + CreatedOnToolsVersion = 14.2; + }; B3579D0E2106B11D00DDEBD6 = { CreatedOnToolsVersion = 10.0; LastSwiftMigration = 1020; @@ -1658,11 +1705,19 @@ B3579D0E2106B11D00DDEBD6 /* PVLibrary */, B3C04B602107867200E7AF79 /* PVLibraryTests */, B3FAC8DA292B1DDD005E8B11 /* PVPatreon */, + B31ACB33294DBBBC002D7E2F /* PVPatreonTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + B31ACB32294DBBBC002D7E2F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; B3579D0D2106B11D00DDEBD6 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1689,6 +1744,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + B31ACB30294DBBBC002D7E2F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B31ACB37294DBBBD002D7E2F /* PVPatreonTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B3579D0B2106B11D00DDEBD6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1889,19 +1952,26 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3FAC8E9292B1E1B005E8B11 /* Patron.swift in Sources */, - B3FAC8ED292B1E1B005E8B11 /* PatreonAPI.swift in Sources */, - B3FAC8EC292B1E1B005E8B11 /* PatreonAccount.swift in Sources */, - B3FAC8EE292B1E1B005E8B11 /* Tier.swift in Sources */, - B3FAC8EA292B1E1B005E8B11 /* Campaign.swift in Sources */, - B3FAC8EB292B1E1B005E8B11 /* Benefit.swift in Sources */, - B3FAC8F6292B2551005E8B11 /* Keychain.swift in Sources */, + B3E7ADEA294DC45F00548C24 /* PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift in Sources */, + B3E7ADE7294DC37900548C24 /* PatreonAPI.swift in Sources */, + B3E7ADEF294DC83500548C24 /* Campaign.swift in Sources */, + B3E7ADE8294DC41A00548C24 /* Consts.swift in Sources */, + B3E7ADF2294DCACD00548C24 /* PatreonAccount.swift in Sources */, + B3E7ADED294DC7A200548C24 /* Tier.swift in Sources */, + B3E7ADF1294DC97500548C24 /* Patron.swift in Sources */, + B3E7ADEC294DC79000548C24 /* Benefit.swift in Sources */, + B31ACB43294DBE02002D7E2F /* Keychain.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + B31ACB3A294DBBBD002D7E2F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B3FAC8DA292B1DDD005E8B11 /* PVPatreon */; + targetProxy = B31ACB39294DBBBD002D7E2F /* PBXContainerItemProxy */; + }; B3C04B682107867200E7AF79 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B3579D0E2106B11D00DDEBD6 /* PVLibrary */; @@ -1915,6 +1985,68 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + B31ACB3C294DBBBD002D7E2F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.PVPatreonTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B31ACB3D294DBBBD002D7E2F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.PVPatreonTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + B31ACB3E294DBBBD002D7E2F /* Archive */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.provenance-emu.PVPatreonTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Archive; + }; B324C5352191A78D009F4EDC /* Archive */ = { isa = XCBuildConfiguration; baseConfigurationReference = B317380127841959002D3ACD /* Build.xcconfig */; @@ -2322,6 +2454,7 @@ }; B3FAC8DF292B1DDD005E8B11 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B31ACB27294DA388002D7E2F /* PatreonConfig.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; @@ -2333,6 +2466,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PVPatreon/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Provenance Emu. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -2363,6 +2497,7 @@ }; B3FAC8E0292B1DDD005E8B11 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B31ACB27294DA388002D7E2F /* PatreonConfig.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; @@ -2375,6 +2510,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PVPatreon/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Provenance Emu. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -2404,6 +2540,7 @@ }; B3FAC8E1292B1DDD005E8B11 /* Archive */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B31ACB27294DA388002D7E2F /* PatreonConfig.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; @@ -2416,6 +2553,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PVPatreon/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Provenance Emu. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -2446,6 +2584,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + B31ACB3B294DBBBD002D7E2F /* Build configuration list for PBXNativeTarget "PVPatreonTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B31ACB3C294DBBBD002D7E2F /* Debug */, + B31ACB3D294DBBBD002D7E2F /* Release */, + B31ACB3E294DBBBD002D7E2F /* Archive */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B3579D092106B11D00DDEBD6 /* Build configuration list for PBXProject "PVLibrary" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/PVLibrary/PVLibrary.xcodeproj/xcshareddata/xcschemes/PVPatreon.xcscheme b/PVLibrary/PVLibrary.xcodeproj/xcshareddata/xcschemes/PVPatreon.xcscheme index 841c5c5f87..f7a01b17a1 100644 --- a/PVLibrary/PVLibrary.xcodeproj/xcshareddata/xcschemes/PVPatreon.xcscheme +++ b/PVLibrary/PVLibrary.xcodeproj/xcshareddata/xcschemes/PVPatreon.xcscheme @@ -28,6 +28,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + +{ + public let key: String + + public var wrappedValue: Value? { + get { + switch Value.self + { + case is Data.Type: return try? Keychain.shared.keychain.getData(self.key) as? Value + case is String.Type: return try? Keychain.shared.keychain.getString(self.key) as? Value + default: return nil + } + } + set { + switch Value.self + { + case is Data.Type: Keychain.shared.keychain[data: self.key] = newValue as? Data + case is String.Type: Keychain.shared.keychain[self.key] = newValue as? String + default: break + } + } + } + + public init(key: String) + { + self.key = key + } +} + +public class Keychain +{ + public static let shared = Keychain() + + fileprivate let keychain = KeychainAccess.Keychain(service: "org.provenance-emu.provenance").accessibility(.afterFirstUnlock).synchronizable(true) + +// @KeychainItem(key: "appleIDEmailAddress") +// public var appleIDEmailAddress: String? +// +// @KeychainItem(key: "appleIDPassword") +// public var appleIDPassword: String? +// +// @KeychainItem(key: "signingCertificatePrivateKey") +// public var signingCertificatePrivateKey: Data? +// +// @KeychainItem(key: "signingCertificateSerialNumber") +// public var signingCertificateSerialNumber: String? +// +// @KeychainItem(key: "signingCertificate") +// public var signingCertificate: Data? +// +// @KeychainItem(key: "signingCertificatePassword") +// public var signingCertificatePassword: String? + + @KeychainItem(key: "patreonAccessToken") + public var patreonAccessToken: String? + + @KeychainItem(key: "patreonRefreshToken") + public var patreonRefreshToken: String? + + @KeychainItem(key: "patreonCreatorAccessToken") + public var patreonCreatorAccessToken: String? + + @KeychainItem(key: "patreonAccountID") + public var patreonAccountID: String? + + private init() + { + } + + public func reset() + { +// self.appleIDEmailAddress = nil +// self.appleIDPassword = nil +// self.signingCertificatePrivateKey = nil +// self.signingCertificateSerialNumber = nil + } +} diff --git a/PVLibrary/PVPatreon/Benefit.swift b/PVLibrary/PVPatreon/Benefit.swift new file mode 100755 index 0000000000..e06ff46b09 --- /dev/null +++ b/PVLibrary/PVPatreon/Benefit.swift @@ -0,0 +1,38 @@ +// +// Benefit.swift +// AltStore +// +// Created by Riley Testut on 8/21/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +public enum PVPatreonBenefitType: String { + case betaAccess = "7585304" + case credit = "8490206" +} + +@available(iOS 12.0, tvOS 12.0, *) +extension PatreonAPI +{ + struct BenefitResponse: Decodable + { + var id: String + } +} + +@available(iOS 12.0, tvOS 12.0, *) +public struct Benefit: Hashable +{ + public var type: PVPatreonBenefitType + + init?(response: PatreonAPI.BenefitResponse) + { + guard let type = PVPatreonBenefitType(rawValue: response.id) else { + ELOG("Unknown benefit id \(response.id)") + return nil + } + self.type = type + } +} diff --git a/PVLibrary/PVPatreon/Campaign.swift b/PVLibrary/PVPatreon/Campaign.swift new file mode 100755 index 0000000000..7b6a23b745 --- /dev/null +++ b/PVLibrary/PVPatreon/Campaign.swift @@ -0,0 +1,29 @@ +// +// Campaign.swift +// AltStore +// +// Created by Riley Testut on 8/21/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +@available(iOS 12.0, tvOS 12.0, *) +extension PatreonAPI +{ + struct CampaignResponse: Decodable + { + var id: String + } +} + +@available(iOS 12.0, tvOS 12.0, *) +public struct Campaign +{ + public var identifier: String + + init(response: PatreonAPI.CampaignResponse) + { + self.identifier = response.id + } +} diff --git a/PVLibrary/PVPatreon/Consts.swift b/PVLibrary/PVPatreon/Consts.swift new file mode 100644 index 0000000000..2745eeb310 --- /dev/null +++ b/PVLibrary/PVPatreon/Consts.swift @@ -0,0 +1,36 @@ +// +// Consts.swift +// PVPatreon +// +// Created by Joseph Mattiello on 12/17/22. +// Copyright © 2022 Provenance Emu. All rights reserved. +// + +import Foundation + +private class PVPatreonHandle: NSObject { } +internal enum Const { + static let bundle: Bundle = Bundle(for: PVPatreonHandle.self) + static let patreon: [String:String] = { bundle.infoDictionary!["PATREON"] as! [String:String] }() + static func string(forKey key: String) -> String { + patreon[key] ?? "" + } + static internal let clientID: String = { + string(forKey:"CLIENT_ID") + }() + static internal let clientSecret: String = { + string(forKey:"CLIENT_SECRET") + }() + static internal let campaignID: String = { + string(forKey:"CAMPAIGN_ID") + }() + static internal let redirectURL: String = { + string(forKey:"REDIRECT_URL") + }() + static internal let callbackURLScheme: String = { + string(forKey:"CALLBACK_URL_SCHEME") + }() + static internal let keychainService: String = { + bundle.infoDictionary!["KEYCHAIN_SERIVCE"] as! String + }() +} diff --git a/PVLibrary/PVPatreon/Info.plist b/PVLibrary/PVPatreon/Info.plist new file mode 100644 index 0000000000..8a3853315f --- /dev/null +++ b/PVLibrary/PVPatreon/Info.plist @@ -0,0 +1,21 @@ + + + + + KEYCHAIN_SERIVCE + $(KEYCHAIN_SERIVCE) + PATREON + + CALLBACK_URL_SCHEME + $(CALLBACK_URL_SCHEME) + REDIRECT_URL + $(REDIRECT_URL) + CLIENT_ID + $(CLIENT_ID) + CLIENT_SECRET + $(CLIENT_SECRET) + CAMPAIGN_ID + $(CAMPAIGN_ID) + + + diff --git a/PVLibrary/PVPatreon/PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift b/PVLibrary/PVPatreon/PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift new file mode 100644 index 0000000000..ce26f6d0f8 --- /dev/null +++ b/PVLibrary/PVPatreon/PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift @@ -0,0 +1,21 @@ +// +// PatreonAPI+ASWebAuthenticationPresentationContextProviding.swift +// PVPatreon +// +// Created by Joseph Mattiello on 12/17/22. +// Copyright © 2022 Provenance Emu. All rights reserved. +// + +import Foundation +#if canImport(AuthenticationServices) +import AuthenticationServices + +@available(tvOS 16.0, *) +extension PatreonAPI: ASWebAuthenticationPresentationContextProviding +{ + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor + { + return UIApplication.shared.keyWindow ?? UIWindow() + } +} +#endif diff --git a/PVLibrary/PVPatreon/PatreonAPI.swift b/PVLibrary/PVPatreon/PatreonAPI.swift new file mode 100755 index 0000000000..908c84150a --- /dev/null +++ b/PVLibrary/PVPatreon/PatreonAPI.swift @@ -0,0 +1,434 @@ +// +// PatreonAPI.swift +// AltStore +// +// Created by Riley Testut on 8/20/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import AuthenticationServices +import CoreData + +private let clientID = "nSNDsv4K_SHF_kLfNgjTi52cU2bTuwunxu9g6j61WtQxoaGEHy1aNAZydM4VcMiz" +private let clientSecret = "QkHx9MirO0QYvVcJzrsoRU5IO9qusihvbwaXVQRlUohnS631CQKunSkDDVAnJbkZ" + +private let campaignID = "2198356" + +@available(iOS 12.0, tvOS 12.0, *) +extension PatreonAPI +{ + enum Error: LocalizedError + { + case unknown + case notAuthenticated + case invalidAccessToken + + var errorDescription: String? { + switch self + { + case .unknown: return NSLocalizedString("An unknown error occurred.", comment: "") + case .notAuthenticated: return NSLocalizedString("No connected Patreon account.", comment: "") + case .invalidAccessToken: return NSLocalizedString("Invalid access token.", comment: "") + } + } + } + + enum AuthorizationType + { + case none + case user + case creator + } + + enum AnyResponse: Decodable + { + case tier(TierResponse) + case benefit(BenefitResponse) + + enum CodingKeys: String, CodingKey + { + case type + } + + init(from decoder: Decoder) throws + { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(String.self, forKey: .type) + switch type + { + case "tier": + let tier = try TierResponse(from: decoder) + self = .tier(tier) + + case "benefit": + let benefit = try BenefitResponse(from: decoder) + self = .benefit(benefit) + + default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unrecognized Patreon response type.") + } + } + } +} + +@available(iOS 12.0, tvOS 12.0, *) +public class PatreonAPI: NSObject +{ + public static let shared = PatreonAPI() + + public var isAuthenticated: Bool { + return Keychain.shared.patreonAccessToken != nil + } + + private var authenticationSession: ASWebAuthenticationSession? + + private let session = URLSession(configuration: .ephemeral) + private let baseURL = URL(string: "https://www.patreon.com/")! + + private override init() + { + super.init() + } +} + +@available(iOS 12.0, tvOS 12.0, *) +public extension PatreonAPI +{ + func authenticate(completion: @escaping (Result) -> Void) + { + var components = URLComponents(string: "/oauth2/authorize")! + components.queryItems = [URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "client_id", value: clientID), + URLQueryItem(name: "redirect_uri", value: "https://rileytestut.com/patreon/altstore")] + + let requestURL = components.url(relativeTo: self.baseURL)! + + self.authenticationSession = ASWebAuthenticationSession(url: requestURL, callbackURLScheme: "altstore") { (callbackURL, error) in + do + { + let callbackURL = try Result(callbackURL, error).get() + + guard + let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false), + let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), + let code = codeQueryItem.value + else { throw Error.unknown } + + self.fetchAccessToken(oauthCode: code) { (result) in + switch result + { + case .failure(let error): completion(.failure(error)) + case .success((let accessToken, let refreshToken)): + Keychain.shared.patreonAccessToken = accessToken + Keychain.shared.patreonRefreshToken = refreshToken + + self.fetchAccount { (result) in + switch result + { + case .success(let account): Keychain.shared.patreonAccountID = account.identifier + case .failure: break + } + + completion(result) + } + + } + } + } + catch + { + completion(.failure(error)) + } + } + + if #available(iOS 13.0, *) + { + self.authenticationSession?.presentationContextProvider = self + } + + self.authenticationSession?.start() + } + + func fetchAccount(completion: @escaping (Result) -> Void) + { + var components = URLComponents(string: "/api/oauth2/v2/identity")! + components.queryItems = [URLQueryItem(name: "include", value: "memberships"), + URLQueryItem(name: "fields[user]", value: "first_name,full_name"), + URLQueryItem(name: "fields[member]", value: "full_name,patron_status")] + + let requestURL = components.url(relativeTo: self.baseURL)! + let request = URLRequest(url: requestURL) + + self.send(request, authorizationType: .user) { (result: Result) in + switch result + { + case .failure(Error.notAuthenticated): + self.signOut() { (result) in + completion(.failure(Error.notAuthenticated)) + } + + case .failure(let error): completion(.failure(error)) + case .success(let response): + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + let account = PatreonAccount(response: response, context: context) + completion(.success(account)) + } + } + } + } + + func fetchPatrons(completion: @escaping (Result<[Patron], Swift.Error>) -> Void) + { + var components = URLComponents(string: "/api/oauth2/v2/campaigns/\(campaignID)/members")! + components.queryItems = [URLQueryItem(name: "include", value: "currently_entitled_tiers,currently_entitled_tiers.benefits"), + URLQueryItem(name: "fields[tier]", value: "title"), + URLQueryItem(name: "fields[member]", value: "full_name,patron_status"), + URLQueryItem(name: "page[size]", value: "1000")] + + let requestURL = components.url(relativeTo: self.baseURL)! + + struct Response: Decodable + { + var data: [PatronResponse] + var included: [AnyResponse] + var links: [String: URL]? + } + + var allPatrons = [Patron]() + + func fetchPatrons(url: URL) + { + let request = URLRequest(url: url) + + self.send(request, authorizationType: .creator) { (result: Result) in + switch result + { + case .failure(let error): completion(.failure(error)) + case .success(let response): + let tiers = response.included.compactMap { (response) -> Tier? in + switch response + { + case .tier(let tierResponse): return Tier(response: tierResponse) + case .benefit: return nil + } + } + + let tiersByIdentifier = Dictionary(tiers.map { ($0.identifier, $0) }, uniquingKeysWith: { (a, b) in return a }) + + let patrons = response.data.map { (response) -> Patron in + let patron = Patron(response: response) + + for tierID in response.relationships?.currently_entitled_tiers.data ?? [] + { + guard let tier = tiersByIdentifier[tierID.id] else { continue } + patron.benefits.formUnion(tier.benefits) + } + + return patron + }.filter { $0.benefits.contains(where: { $0.type == .credits }) } + + allPatrons.append(contentsOf: patrons) + + if let nextURL = response.links?["next"] + { + fetchPatrons(url: nextURL) + } + else + { + completion(.success(allPatrons)) + } + } + } + } + + fetchPatrons(url: requestURL) + } + + func signOut(completion: @escaping (Result) -> Void) + { + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + do + { + if let account = DatabaseManager.shared.patreonAccount(in: context) { + context.delete(account) + } + + self.deactivateBetaApps(in: context) + + try context.save() + + Keychain.shared.patreonAccessToken = nil + Keychain.shared.patreonRefreshToken = nil + Keychain.shared.patreonAccountID = nil + + completion(.success(())) + } + catch + { + completion(.failure(error)) + } + } + } + + func refreshPatreonAccount() + { + guard PatreonAPI.shared.isAuthenticated else { return } + + PatreonAPI.shared.fetchAccount { (result: Result) in + do + { + let account = try result.get() + + if let context = account.managedObjectContext, !account.isPatron + { + // Deactivate all beta apps now that we're no longer a patron. + self.deactivateBetaApps(in: context) + } + + try account.managedObjectContext?.save() + } + catch + { + print("Failed to fetch Patreon account.", error) + } + } + } +} + +@available(iOS 12.0, tvOS 12.0, *) +private extension PatreonAPI +{ + func fetchAccessToken(oauthCode: String, completion: @escaping (Result<(String, String), Swift.Error>) -> Void) + { + let encodedRedirectURI = ("https://provenance-emu.com/patreon_redirect" as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)! + let encodedOauthCode = (oauthCode as NSString).addingPercentEncoding(withAllowedCharacters: .alphanumerics)! + + let body = "code=\(encodedOauthCode)&grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(encodedRedirectURI)" + + let requestURL = URL(string: "/api/oauth2/token", relativeTo: self.baseURL)! + + var request = URLRequest(url: requestURL) + request.httpMethod = "POST" + request.httpBody = body.data(using: .utf8) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + struct Response: Decodable + { + var access_token: String + var refresh_token: String + } + + self.send(request, authorizationType: .none) { (result: Result) in + switch result + { + case .failure(let error): completion(.failure(error)) + case .success(let response): completion(.success((response.access_token, response.refresh_token))) + } + } + } + + func refreshAccessToken(completion: @escaping (Result) -> Void) + { + guard let refreshToken = Keychain.shared.patreonRefreshToken else { return } + + var components = URLComponents(string: "/api/oauth2/token")! + components.queryItems = [URLQueryItem(name: "grant_type", value: "refresh_token"), + URLQueryItem(name: "refresh_token", value: refreshToken), + URLQueryItem(name: "client_id", value: clientID), + URLQueryItem(name: "client_secret", value: clientSecret)] + + let requestURL = components.url(relativeTo: self.baseURL)! + + var request = URLRequest(url: requestURL) + request.httpMethod = "POST" + + struct Response: Decodable + { + var access_token: String + var refresh_token: String + } + + self.send(request, authorizationType: .none) { (result: Result) in + switch result + { + case .failure(let error): completion(.failure(error)) + case .success(let response): + Keychain.shared.patreonAccessToken = response.access_token + Keychain.shared.patreonRefreshToken = response.refresh_token + + completion(.success(())) + } + } + } + + func send(_ request: URLRequest, authorizationType: AuthorizationType, completion: @escaping (Result) -> Void) + { + var request = request + + switch authorizationType + { + case .none: break + case .creator: + guard let creatorAccessToken = Keychain.shared.patreonCreatorAccessToken else { return completion(.failure(Error.invalidAccessToken)) } + request.setValue("Bearer " + creatorAccessToken, forHTTPHeaderField: "Authorization") + + case .user: + guard let accessToken = Keychain.shared.patreonAccessToken else { return completion(.failure(Error.notAuthenticated)) } + request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") + } + + let task = self.session.dataTask(with: request) { (data, response, error) in + do + { + let data = try Result(data, error).get() + + if let response = response as? HTTPURLResponse, response.statusCode == 401 + { + switch authorizationType + { + case .creator: completion(.failure(Error.invalidAccessToken)) + case .none: completion(.failure(Error.notAuthenticated)) + case .user: + self.refreshAccessToken() { (result) in + switch result + { + case .failure(let error): completion(.failure(error)) + case .success: self.send(request, authorizationType: authorizationType, completion: completion) + } + } + } + + return + } + + let response = try JSONDecoder().decode(ResponseType.self, from: data) + completion(.success(response)) + } + catch let error + { + completion(.failure(error)) + } + } + + task.resume() + } + + func deactivateBetaApps(in context: NSManagedObjectContext) + { + let predicate = NSPredicate(format: "%K != %@ AND %K != nil AND %K == YES", + #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID, #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.isBeta)) + + let installedApps = InstalledApp.all(satisfying: predicate, in: context) + installedApps.forEach { $0.isActive = false } + } +} + +@available(iOS 13.0, *) +extension PatreonAPI: ASWebAuthenticationPresentationContextProviding +{ + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor + { + return UIApplication.shared.keyWindow ?? UIWindow() + } +} diff --git a/PVLibrary/PVPatreon/PatreonAccount.swift b/PVLibrary/PVPatreon/PatreonAccount.swift new file mode 100755 index 0000000000..b46a65c453 --- /dev/null +++ b/PVLibrary/PVPatreon/PatreonAccount.swift @@ -0,0 +1,55 @@ +// +// PatreonAccount.swift +// AltStore +// +// Created by Riley Testut on 8/20/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import CoreData + +@available(iOS 12.0, tvOS 12.0, *) +extension PatreonAPI +{ + struct AccountResponse: Decodable + { + struct Data: Decodable + { + struct Attributes: Decodable + { + var first_name: String? + var full_name: String + } + + var id: String + var attributes: Attributes + } + + var data: Data + var included: [PatronResponse]? + } +} + +@available(iOS 12.0, tvOS 12.0, *) +public struct PatreonAccount: Codable +{ + public let identifier: String + + public let name: String + public let firstName: String? + + public let isPatron: Bool + + init(response: PatreonAPI.AccountResponse) { + self.identifier = response.data.id + self.name = response.data.attributes.full_name + self.firstName = response.data.attributes.first_name + + if let patronResponse = response.included?.first { + let patron = Patron(response: patronResponse) + self.isPatron = (patron.status == .active) + } else { + self.isPatron = false + } + } +} diff --git a/PVLibrary/PVPatreon/PatreonConfig.xcconfig b/PVLibrary/PVPatreon/PatreonConfig.xcconfig new file mode 100644 index 0000000000..2d90ebf225 --- /dev/null +++ b/PVLibrary/PVPatreon/PatreonConfig.xcconfig @@ -0,0 +1,16 @@ +// +// Config.xcconfig +// PVLibrary +// +// Created by Joseph Mattiello on 12/17/22. +// Copyright © 2022 Provenance Emu. All rights reserved. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 +CLIENT_SECRET = "QkHx9MirO0QYvVcJzrsoRU5IO9qusihvbwaXVQRlUohnS631CQKunSkDDVAnJbkZ" +CLIENT_ID = "nSNDsv4K_SHF_kLfNgjTi52cU2bTuwunxu9g6j61WtQxoaGEHy1aNAZydM4VcMiz" +CAMPAIGN_ID = "2198356" +KEYCHAIN_SERIVCE = "org.provenance-emu.provenance" +REDIRECT_URL = "https://rileytestut.com/patreon/altstore" +CALLBACK_URL_SCHEME = "altstore" diff --git a/PVLibrary/PVPatreon/Patron.swift b/PVLibrary/PVPatreon/Patron.swift new file mode 100755 index 0000000000..d716b86c52 --- /dev/null +++ b/PVLibrary/PVPatreon/Patron.swift @@ -0,0 +1,78 @@ +// +// Patron.swift +// AltStore +// +// Created by Riley Testut on 8/21/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +extension PatreonAPI +{ + struct PatronResponse: Decodable + { + struct Attributes: Decodable + { + var full_name: String + var patron_status: String? + } + + struct Relationships: Decodable + { + struct Tiers: Decodable + { + struct TierID: Decodable + { + var id: String + var type: String + } + + var data: [TierID] + } + + var currently_entitled_tiers: Tiers + } + + var id: String + var attributes: Attributes + + var relationships: Relationships? + } +} + +extension Patron +{ + public enum Status: String, Decodable + { + case active = "active_patron" + case declined = "declined_patron" + case former = "former_patron" + case unknown = "unknown" + } +} + +public class Patron +{ + public var name: String + public var identifier: String + + public var status: Status + + public var benefits: Set = [] + + init(response: PatreonAPI.PatronResponse) + { + self.name = response.attributes.full_name + self.identifier = response.id + + if let status = response.attributes.patron_status + { + self.status = Status(rawValue: status) ?? .unknown + } + else + { + self.status = .unknown + } + } +} diff --git a/PVLibrary/PVPatreon/Tier.swift b/PVLibrary/PVPatreon/Tier.swift new file mode 100755 index 0000000000..422f2d3989 --- /dev/null +++ b/PVLibrary/PVPatreon/Tier.swift @@ -0,0 +1,52 @@ +// +// Tier.swift +// AltStore +// +// Created by Riley Testut on 8/21/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +@available(iOS 12.0, tvOS 12.0, *) +extension PatreonAPI +{ + struct TierResponse: Decodable + { + struct Attributes: Decodable + { + var title: String + } + + struct Relationships: Decodable + { + struct Benefits: Decodable + { + var data: [BenefitResponse] + } + + var benefits: Benefits + } + + var id: String + var attributes: Attributes + + var relationships: Relationships + } +} + +@available(iOS 12.0, tvOS 12.0, *) +public struct Tier +{ + public var name: String + public var identifier: String + + public var benefits: [Benefit] = [] + + init(response: PatreonAPI.TierResponse) + { + self.name = response.attributes.title + self.identifier = response.id + self.benefits = response.relationships.benefits.data.map(Benefit.init(response:)) + } +} diff --git a/PVLibrary/PVPatreonTests/PVPatreonTests.swift b/PVLibrary/PVPatreonTests/PVPatreonTests.swift new file mode 100644 index 0000000000..f8f31de3aa --- /dev/null +++ b/PVLibrary/PVPatreonTests/PVPatreonTests.swift @@ -0,0 +1,35 @@ +// +// PVPatreonTests.swift +// PVPatreonTests +// +// Created by Joseph Mattiello on 12/17/22. +// Copyright © 2022 Provenance Emu. All rights reserved. +// + +import XCTest +@testable import PVPatreon + +final class PVPatreonTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testPatreonBundleInfo() throws { +// let clientID = PVPatreon.Const.clientID +// let clientSecret = PVPatreon.Const.clientSecret +// let campaignID = PVPatreon.Const.campaignID + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +}