diff --git a/Example-tvOS/.gitignore b/Example-tvOS/.gitignore new file mode 100644 index 00000000..42a22df4 --- /dev/null +++ b/Example-tvOS/.gitignore @@ -0,0 +1,24 @@ +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +# Pods are ignored in the samples as all Pods & their dependencies are either +# development Pods (this repo) or sourced from repos in the same organization. +# Generally we recommend versioning Pods, see the pros & cons here: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +Pods diff --git a/Example-tvOS/Example-tvOS.xcodeproj/project.pbxproj b/Example-tvOS/Example-tvOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000..64624d2e --- /dev/null +++ b/Example-tvOS/Example-tvOS.xcodeproj/project.pbxproj @@ -0,0 +1,382 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 342EE8741DE025E3008D4DBC /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 342EE8731DE025E3008D4DBC /* main.m */; }; + 342EE8771DE025E3008D4DBC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 342EE8761DE025E3008D4DBC /* AppDelegate.m */; }; + 342EE87A1DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 342EE8791DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.m */; }; + 342EE87D1DE025E3008D4DBC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 342EE87B1DE025E3008D4DBC /* Main.storyboard */; }; + 342EE87F1DE025E3008D4DBC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 342EE87E1DE025E3008D4DBC /* Assets.xcassets */; }; + 797A0932433ABAD7CA6E074D /* libPods-Example-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A4F90BF033C6C0DD262D899 /* libPods-Example-tvOS.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0A4F90BF033C6C0DD262D899 /* libPods-Example-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Example-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 342EE86F1DE025E3008D4DBC /* Example-tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 342EE8731DE025E3008D4DBC /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 342EE8751DE025E3008D4DBC /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 342EE8761DE025E3008D4DBC /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 342EE8781DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMAppAuthTVExampleViewController.h; sourceTree = ""; }; + 342EE8791DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMAppAuthTVExampleViewController.m; sourceTree = ""; }; + 342EE87C1DE025E3008D4DBC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 342EE87E1DE025E3008D4DBC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 342EE8801DE025E3008D4DBC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B4712131AD155065B2BAEAF1 /* Pods-Example-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Example-tvOS/Pods-Example-tvOS.release.xcconfig"; sourceTree = ""; }; + B7838D82B7D2E66A35C0A269 /* Pods-Example-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Example-tvOS/Pods-Example-tvOS.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 342EE86C1DE025E3008D4DBC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 797A0932433ABAD7CA6E074D /* libPods-Example-tvOS.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 28E701CA92357FADFF46058D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0A4F90BF033C6C0DD262D899 /* libPods-Example-tvOS.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 342EE8661DE025E3008D4DBC = { + isa = PBXGroup; + children = ( + 342EE8711DE025E3008D4DBC /* Source */, + 342EE8701DE025E3008D4DBC /* Products */, + C98FB57E1D52E482C6C8832B /* Pods */, + 28E701CA92357FADFF46058D /* Frameworks */, + ); + sourceTree = ""; + }; + 342EE8701DE025E3008D4DBC /* Products */ = { + isa = PBXGroup; + children = ( + 342EE86F1DE025E3008D4DBC /* Example-tvOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 342EE8711DE025E3008D4DBC /* Source */ = { + isa = PBXGroup; + children = ( + 342EE8751DE025E3008D4DBC /* AppDelegate.h */, + 342EE8761DE025E3008D4DBC /* AppDelegate.m */, + 342EE8781DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.h */, + 342EE8791DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.m */, + 342EE87B1DE025E3008D4DBC /* Main.storyboard */, + 342EE87E1DE025E3008D4DBC /* Assets.xcassets */, + 342EE8801DE025E3008D4DBC /* Info.plist */, + 342EE8721DE025E3008D4DBC /* Supporting Files */, + ); + path = Source; + sourceTree = ""; + }; + 342EE8721DE025E3008D4DBC /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 342EE8731DE025E3008D4DBC /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + C98FB57E1D52E482C6C8832B /* Pods */ = { + isa = PBXGroup; + children = ( + B7838D82B7D2E66A35C0A269 /* Pods-Example-tvOS.debug.xcconfig */, + B4712131AD155065B2BAEAF1 /* Pods-Example-tvOS.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 342EE86E1DE025E3008D4DBC /* Example-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 342EE8831DE025E3008D4DBC /* Build configuration list for PBXNativeTarget "Example-tvOS" */; + buildPhases = ( + 4A65CE84FDD0F3F6AA535686 /* [CP] Check Pods Manifest.lock */, + 342EE86B1DE025E3008D4DBC /* Sources */, + 342EE86C1DE025E3008D4DBC /* Frameworks */, + 342EE86D1DE025E3008D4DBC /* Resources */, + 9357F0361518BC850E8E1DA9 /* [CP] Embed Pods Frameworks */, + 0A976A3A7AD11A6010FBA5B2 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Example-tvOS"; + productName = "Example-tvOS"; + productReference = 342EE86F1DE025E3008D4DBC /* Example-tvOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 342EE8671DE025E3008D4DBC /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Google; + TargetAttributes = { + 342EE86E1DE025E3008D4DBC = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 342EE86A1DE025E3008D4DBC /* Build configuration list for PBXProject "Example-tvOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 342EE8661DE025E3008D4DBC; + productRefGroup = 342EE8701DE025E3008D4DBC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 342EE86E1DE025E3008D4DBC /* Example-tvOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 342EE86D1DE025E3008D4DBC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 342EE87F1DE025E3008D4DBC /* Assets.xcassets in Resources */, + 342EE87D1DE025E3008D4DBC /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0A976A3A7AD11A6010FBA5B2 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example-tvOS/Pods-Example-tvOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 4A65CE84FDD0F3F6AA535686 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 9357F0361518BC850E8E1DA9 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example-tvOS/Pods-Example-tvOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 342EE86B1DE025E3008D4DBC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 342EE87A1DE025E3008D4DBC /* GTMAppAuthTVExampleViewController.m in Sources */, + 342EE8771DE025E3008D4DBC /* AppDelegate.m in Sources */, + 342EE8741DE025E3008D4DBC /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 342EE87B1DE025E3008D4DBC /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 342EE87C1DE025E3008D4DBC /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 342EE8811DE025E3008D4DBC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + 342EE8821DE025E3008D4DBC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 10.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 342EE8841DE025E3008D4DBC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B7838D82B7D2E66A35C0A269 /* Pods-Example-tvOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Source/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.Example-tvOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 342EE8851DE025E3008D4DBC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B4712131AD155065B2BAEAF1 /* Pods-Example-tvOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = Source/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.Example-tvOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 342EE86A1DE025E3008D4DBC /* Build configuration list for PBXProject "Example-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 342EE8811DE025E3008D4DBC /* Debug */, + 342EE8821DE025E3008D4DBC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 342EE8831DE025E3008D4DBC /* Build configuration list for PBXNativeTarget "Example-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 342EE8841DE025E3008D4DBC /* Debug */, + 342EE8851DE025E3008D4DBC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 342EE8671DE025E3008D4DBC /* Project object */; +} diff --git a/Example-tvOS/Podfile b/Example-tvOS/Podfile new file mode 100644 index 00000000..6ef3b340 --- /dev/null +++ b/Example-tvOS/Podfile @@ -0,0 +1,10 @@ +target 'Example-tvOS' do + platform :tvos, '9.0' + + # Pods for GTMAppAuth development + pod 'GTMAppAuth', :path => '../' + + # In production, you would use: + # pod 'GTMAppAuth' + +end diff --git a/Example-tvOS/Source/AppDelegate.h b/Example-tvOS/Source/AppDelegate.h new file mode 100644 index 00000000..9804e056 --- /dev/null +++ b/Example-tvOS/Source/AppDelegate.h @@ -0,0 +1,26 @@ +/*! @file AppDelegate.h + @brief GTMAppAuth tvOS SDK Example + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end + diff --git a/Example-tvOS/Source/AppDelegate.m b/Example-tvOS/Source/AppDelegate.m new file mode 100644 index 00000000..185bfaaa --- /dev/null +++ b/Example-tvOS/Source/AppDelegate.m @@ -0,0 +1,47 @@ +/*! @file AppDelegate.m + @brief GTMAppAuth tvOS SDK Example + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { +} + +- (void)applicationWillTerminate:(UIApplication *)application { +} + +@end diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json new file mode 100644 index 00000000..8bf75d9f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json new file mode 100644 index 00000000..8bf75d9f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000..6d596bc7 --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "size" : "1280x768", + "idiom" : "tv", + "filename" : "App Icon - Large.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "400x240", + "idiom" : "tv", + "filename" : "App Icon - Small.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "2320x720", + "idiom" : "tv", + "filename" : "Top Shelf Image Wide.imageset", + "role" : "top-shelf-image-wide" + }, + { + "size" : "1920x720", + "idiom" : "tv", + "filename" : "Top Shelf Image.imageset", + "role" : "top-shelf-image" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/Contents.json b/Example-tvOS/Source/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Assets.xcassets/LaunchImage.launchimage/Contents.json b/Example-tvOS/Source/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000..29d94c78 --- /dev/null +++ b/Example-tvOS/Source/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example-tvOS/Source/Base.lproj/Main.storyboard b/Example-tvOS/Source/Base.lproj/Main.storyboard new file mode 100644 index 00000000..2971ec93 --- /dev/null +++ b/Example-tvOS/Source/Base.lproj/Main.storyboard @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example-tvOS/Source/GTMAppAuthTVExampleViewController.h b/Example-tvOS/Source/GTMAppAuthTVExampleViewController.h new file mode 100644 index 00000000..0cc457ed --- /dev/null +++ b/Example-tvOS/Source/GTMAppAuthTVExampleViewController.h @@ -0,0 +1,57 @@ +/*! @file GTMAppAuthTVExampleViewController.h + @brief GTMAppAuth tvOS SDK Example + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@class GTMAppAuthFetcherAuthorization; + +/*! @brief An example app that uses the TV authorization flow to obtain authorization from the user + and make an authorized API call. + */ +@interface GTMAppAuthTVExampleViewController : UIViewController { + IBOutlet UIView *signInView; + IBOutlet UILabel *verificationURLLabel; + IBOutlet UILabel *userCodeLabel; + IBOutlet UIView *signInButtons; + IBOutlet UIButton *cancelSignInButton; + IBOutlet UIView *signedInButtons; + IBOutlet UITextView *logTextView; +} + +/*! @brief The authorization state. + */ +@property(nonatomic, nullable) GTMAppAuthFetcherAuthorization *authorization; + +/*! @brief Initiate the sign-in. + */ +- (IBAction)signin:(nullable id)sender; + +/*! @brief Cancels the active sign-in (if any), has no effect if a sign-in isn't in progress. + */ +- (IBAction)cancelSignIn:(nullable id)sender; + +/*! @brief Forgets the authentication state, used to sign-out the user. + */ +- (IBAction)clearAuthState:(nullable id)sender; + +/*! @brief Performs an authenticated API call. + */ +- (IBAction)userinfo:(nullable id)sender; + +@end + diff --git a/Example-tvOS/Source/GTMAppAuthTVExampleViewController.m b/Example-tvOS/Source/GTMAppAuthTVExampleViewController.m new file mode 100644 index 00000000..65503c53 --- /dev/null +++ b/Example-tvOS/Source/GTMAppAuthTVExampleViewController.m @@ -0,0 +1,244 @@ +/*! @file GTMAppAuthTVExampleViewController.m + @brief GTMAppAuth tvOS SDK Example + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "GTMAppAuthTVExampleViewController.h" + +#import +#import + +#import "GTMSessionFetcher.h" +#import "GTMSessionFetcherService.h" + +/*! @brief The OAuth client ID. + @discussion For Google, register your client at + https://console.developers.google.com/apis/credentials?project=_ + */ +static NSString *const kClientID = @"YOUR_CLIENT.apps.googleusercontent.com"; + +/*! @brief The OAuth client secret. + @discussion For Google, register your client at + https://console.developers.google.com/apis/credentials?project=_ + */ +static NSString *const kClientSecret = @"YOUR_CLIENT_SECRET"; + +/*! @brief NSCoding key for the authorization property. + */ +static NSString *const kExampleAuthorizerKey = @"authorization"; + +@interface GTMAppAuthTVExampleViewController () + +@end + +@implementation GTMAppAuthTVExampleViewController { + GTMTVAuthorizationCancelBlock _cancelBlock; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + +#if !defined(NS_BLOCK_ASSERTIONS) + // NOTE: + // + // To run this sample, you need to register your own Google API client at + // https://console.developers.google.com/apis/credentials?project=_ using the type "Other" + // and update two configuration points in the sample: the kClientID and kRedirectURI constants + // this file. + NSAssert(![kClientID isEqualToString:@"YOUR_CLIENT.apps.googleusercontent.com"], + @"Update kClientID with your own client ID. "); + NSAssert(![kClientSecret isEqualToString:@"YOUR_CLIENT_SECRET"], + @"Update kClientSecret with your own client secret. "); +#endif // !defined(NS_BLOCK_ASSERTIONS) + + logTextView.text = @""; + signInView.hidden = YES; + cancelSignInButton.hidden = YES; + logTextView.selectable = YES; + logTextView.panGestureRecognizer.allowedTouchTypes = @[ @(UITouchTypeIndirect) ]; + + [self loadState]; + [self updateUI]; +} + +- (IBAction)signin:(id)sender { + if (_cancelBlock) { + [self cancelSignIn:nil]; + } + + // builds authentication request + GTMTVServiceConfiguration *configuration = [GTMTVAuthorizationService TVConfigurationForGoogle]; + GTMTVAuthorizationRequest *request = + [[GTMTVAuthorizationRequest alloc] initWithConfiguration:configuration + clientId:kClientID + clientSecret:kClientSecret + scopes:@[ OIDScopeOpenID, OIDScopeProfile ] + additionalParameters:nil]; + + _cancelBlock = [GTMTVAuthorizationService authorizeTVRequest:request + initializaiton:^(GTMTVAuthorizationResponse *_Nullable response, + NSError *_Nullable error) { + if (response) { + [self logMessage:@"Authorization response: %@", response]; + signInView.hidden = NO; + cancelSignInButton.hidden = NO; + verificationURLLabel.text = response.verificationURL; + userCodeLabel.text = response.userCode; + } else { + [self logMessage:@"Initialization error %@", error]; + } + } completion:^(GTMAppAuthFetcherAuthorization *_Nullable authorization, + NSError *_Nullable error) { + signInView.hidden = YES; + if (authorization) { + [self setAuthorization:authorization]; + [self logMessage:@"Token response: %@", authorization.authState.lastTokenResponse]; + } else { + [self setAuthorization:nil]; + [self logMessage:@"Error: %@", error]; + } + }]; +} + +- (IBAction)cancelSignIn:(nullable id)sender { + if (_cancelBlock) { + _cancelBlock(); + _cancelBlock = nil; + } + signInView.hidden = YES; + cancelSignInButton.hidden = YES; +} + +- (void)setAuthorization:(GTMAppAuthFetcherAuthorization*)authorization { + _authorization = authorization; + [self saveState]; + [self updateUI]; +} + +/*! @brief Saves the @c GTMAppAuthFetcherAuthorization to @c NSUSerDefaults. + */ +- (void)saveState { + if (_authorization.canAuthorize) { + [GTMAppAuthFetcherAuthorization saveAuthorization:_authorization + toKeychainForName:kExampleAuthorizerKey]; + } else { + [GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kExampleAuthorizerKey]; + } +} + +/*! @brief Loads the @c GTMAppAuthFetcherAuthorization from @c NSUSerDefaults. + */ +- (void)loadState { + GTMAppAuthFetcherAuthorization* authorization = + [GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kExampleAuthorizerKey]; + [self setAuthorization:authorization]; + if (authorization) { + [self logMessage:@"Authorization restored: %@", authorization]; + } +} + +/*! @brief Refreshes UI, typically called after the auth state changed. + */ +- (void)updateUI { + signInButtons.hidden = [_authorization canAuthorize]; + signedInButtons.hidden = !signInButtons.hidden; +} + +- (IBAction)clearAuthState:(nullable id)sender { + [self setAuthorization:nil]; + [self logMessage:@"Authorization state cleared."]; +} + +- (IBAction)clearLog:(nullable id)sender { + [logTextView.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:@""]]; +} + +- (IBAction)userinfo:(nullable id)sender { + [self logMessage:@"Performing userinfo request"]; + + // Creates a GTMSessionFetcherService with the authorization. + // Normally you would save this service object and re-use it for all REST API calls. + GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init]; + fetcherService.authorizer = self.authorization; + + // Creates a fetcher for the API call. + NSURL *userinfoEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v3/userinfo"]; + GTMSessionFetcher *fetcher = [fetcherService fetcherWithURL:userinfoEndpoint]; + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + + // Checks for an error. + if (error) { + // OIDOAuthTokenErrorDomain indicates an issue with the authorization. + if ([error.domain isEqual:OIDOAuthTokenErrorDomain]) { + [self setAuthorization:nil]; + [self logMessage:@"Authorization error during token refresh, clearing state. %@", error]; + // Other errors are assumed transient. + } else { + [self logMessage:@"Transient error during token refresh. %@", error]; + } + return; + } + + // Parses the JSON response. + NSError *jsonError = nil; + id jsonDictionaryOrArray = + [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + // JSON error. + if (jsonError) { + [self logMessage:@"JSON decoding error %@", jsonError]; + return; + } + + // Success response! + [self logMessage:@"Success: %@", jsonDictionaryOrArray]; + }]; +} + +/*! @brief Logs a message to stdout and the textfield. + @param format The format string and arguments. + */ +- (void)logMessage:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) { + // gets message as string + va_list argp; + va_start(argp, format); + NSString *log = [[NSString alloc] initWithFormat:format arguments:argp]; + va_end(argp); + + // outputs to stdout + NSLog(@"%@", log); + + // appends to output log + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateFormat = @"hh:mm:ss"; + NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; + NSString *logLine = [NSString stringWithFormat:@"\n%@: %@", dateString, log]; + UIFont *systemFont = [UIFont systemFontOfSize:36.0f]; + NSDictionary * fontAttributes = + [[NSDictionary alloc] initWithObjectsAndKeys:systemFont, NSFontAttributeName, nil]; + NSMutableAttributedString* logLineAttr = + [[NSMutableAttributedString alloc] initWithString:logLine attributes:fontAttributes]; + [[logTextView textStorage] appendAttributedString:logLineAttr]; + + // Scroll to bottom + if(logTextView.text.length > 0 ) { + NSRange bottom = NSMakeRange(logTextView.text.length - 1, 1); + [logTextView scrollRangeToVisible:bottom]; + } +} + +@end diff --git a/Example-tvOS/Source/Info.plist b/Example-tvOS/Source/Info.plist new file mode 100644 index 00000000..63dcd6c1 --- /dev/null +++ b/Example-tvOS/Source/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UIUserInterfaceStyle + Automatic + + diff --git a/Example-tvOS/Source/main.m b/Example-tvOS/Source/main.m new file mode 100644 index 00000000..226b2c87 --- /dev/null +++ b/Example-tvOS/Source/main.m @@ -0,0 +1,26 @@ +/*! @file main.m + @brief GTMAppAuth tvOS SDK Example + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Source/GTMAppAuth.h b/Source/GTMAppAuth.h index ecbdad60..2bd5349f 100644 --- a/Source/GTMAppAuth.h +++ b/Source/GTMAppAuth.h @@ -18,6 +18,9 @@ #import "GTMAppAuthFetcherAuthorization.h" #import "GTMAppAuthFetcherAuthorization+Keychain.h" +#import "GTMTVAuthorizationRequest.h" +#import "GTMTVAuthorizationResponse.h" +#import "GTMTVAuthorizationService.h" #if TARGET_OS_TV #elif TARGET_OS_WATCH diff --git a/Source/GTMAppAuthFetcherAuthorization.h b/Source/GTMAppAuthFetcherAuthorization.h index 2290bb1c..0fb74f6b 100644 --- a/Source/GTMAppAuthFetcherAuthorization.h +++ b/Source/GTMAppAuthFetcherAuthorization.h @@ -99,12 +99,12 @@ typedef void (^GTMAppAuthFetcherAuthorizationCompletion)(NSError *_Nullable erro userEmailIsVerified:(nullable NSString *)userEmailIsVerified NS_DESIGNATED_INITIALIZER; -#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +#if !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT /*! @brief Convenience method to return an @c OIDServiceConfiguration for Google. @return A @c OIDServiceConfiguration object setup with Google OAuth endpoints. */ + (OIDServiceConfiguration *)configurationForGoogle; -#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +#endif // !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT /*! @brief Adds an authorization header to the given request, using the authorization state. Refreshes the access token if needed. diff --git a/Source/GTMAppAuthFetcherAuthorization.m b/Source/GTMAppAuthFetcherAuthorization.m index a003e6f2..7c302e67 100644 --- a/Source/GTMAppAuthFetcherAuthorization.m +++ b/Source/GTMAppAuthFetcherAuthorization.m @@ -219,7 +219,7 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { # pragma mark - Convenience -#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +#if !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT + (OIDServiceConfiguration *)configurationForGoogle { NSURL *authorizationEndpoint = [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"]; @@ -231,7 +231,7 @@ + (OIDServiceConfiguration *)configurationForGoogle { tokenEndpoint:tokenEndpoint]; return configuration; } -#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT +#endif // !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT # pragma mark - ID Token extraction diff --git a/Source/GTMTVAuthorizationRequest.h b/Source/GTMTVAuthorizationRequest.h new file mode 100644 index 00000000..b52af825 --- /dev/null +++ b/Source/GTMTVAuthorizationRequest.h @@ -0,0 +1,86 @@ +/*! @file GTMTVAuthorizationRequest.h + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import "OIDAuthorizationRequest.h" + +@class GTMTVServiceConfiguration; + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief Represents a TV and limited input device authorization request. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +@interface GTMTVAuthorizationRequest : OIDAuthorizationRequest + +/*! @brief Designated initializer. + @param configuration The service's configuration. + @param clientID The client identifier. + @param clientSecret The client secret. + @param scope A scope string per the OAuth2 spec (a space-delimited set of scopes). + @param redirectURL The client's redirect URI. + @param responseType The expected response type. + @param state An opaque value used by the client to maintain state between the request and + callback. + @param codeVerifier The PKCE code verifier. See @c OIDAuthorizationRequest.generateCodeVerifier. + @param codeChallenge The PKCE code challenge, calculated from the code verifier such as with + @c OIDAuthorizationRequest.codeChallengeS256ForVerifier:. + @param codeChallengeMethod The PKCE code challenge method. + ::OIDOAuthorizationRequestCodeChallengeMethodS256 when + @c OIDAuthorizationRequest.codeChallengeS256ForVerifier: is used to create the code + challenge. + @param additionalParameters The client's additional authorization parameters. + */ +- (instancetype) + initWithConfiguration:(GTMTVServiceConfiguration *)configuration + clientId:(NSString *)clientID + clientSecret:(nullable NSString *)clientSecret + scope:(nullable NSString *)scope + redirectURL:(NSURL *)redirectURL + responseType:(NSString *)responseType + state:(nullable NSString *)state + codeVerifier:(nullable NSString *)codeVerifier + codeChallenge:(nullable NSString *)codeChallenge + codeChallengeMethod:(nullable NSString *)codeChallengeMethod + additionalParameters:(nullable NSDictionary *)additionalParameters + NS_DESIGNATED_INITIALIZER; + +/*! @brief Creates a TV authorization request with opinionated defaults + @param configuration The service's configuration. + @param clientID The client identifier. + @param clientSecret The client secret. + @param scopes An array of scopes to combine into a single scope string per the OAuth2 spec. + @param TVAuthorizationURL The TV & limited input device authorization endpoint URL. + @param additionalParameters The client's additional authorization parameters. + */ +- (instancetype) + initWithConfiguration:(GTMTVServiceConfiguration *)configuration + clientId:(NSString *)clientID + clientSecret:(NSString *)clientSecret + scopes:(nullable NSArray *)scopes + additionalParameters:(nullable NSDictionary *)additionalParameters; + +/*! @brief Constructs an @c NSURLRequest representing the TV authorization request. + @return An @c NSURLRequest representing the TV authorization request. + */ +- (NSURLRequest *)URLRequest; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/GTMTVAuthorizationRequest.m b/Source/GTMTVAuthorizationRequest.m new file mode 100644 index 00000000..f00d8d55 --- /dev/null +++ b/Source/GTMTVAuthorizationRequest.m @@ -0,0 +1,118 @@ +/*! @file GTMTVAuthorizationRequest.m + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "GTMTVAuthorizationRequest.h" + +#import "OIDScopeUtilities.h" +#import "OIDURLQueryComponent.h" +#import "GTMTVServiceConfiguration.h" + +@implementation GTMTVAuthorizationRequest + +- (instancetype) + initWithConfiguration:(GTMTVServiceConfiguration *)configuration + clientId:(NSString *)clientID + clientSecret:(nullable NSString *)clientSecret + scope:(nullable NSString *)scope + redirectURL:(NSURL *)redirectURL + responseType:(NSString *)responseType + state:(nullable NSString *)state + codeVerifier:(nullable NSString *)codeVerifier + codeChallenge:(nullable NSString *)codeChallenge + codeChallengeMethod:(nullable NSString *)codeChallengeMethod + additionalParameters:(nullable NSDictionary *)additionalParameters { + + if (![configuration isKindOfClass:[GTMTVServiceConfiguration class]]) { + NSAssert([configuration isKindOfClass:[GTMTVServiceConfiguration class]], + @"configuration parameter must be of type GTMTVServiceConfiguration, encountered %@", + NSStringFromClass([configuration class])); + return nil; + } + + return [super initWithConfiguration:configuration + clientId:clientID + clientSecret:clientSecret + scope:scope + redirectURL:redirectURL + responseType:responseType + state:state + codeVerifier:codeVerifier + codeChallenge:codeChallenge + codeChallengeMethod:codeChallengeMethod + additionalParameters:additionalParameters]; +} + +- (instancetype) + initWithConfiguration:(GTMTVServiceConfiguration *)configuration + clientId:(NSString *)clientID + clientSecret:(NSString *)clientSecret + scopes:(nullable NSArray *)scopes + additionalParameters:(nullable NSDictionary *)additionalParameters { + return [self initWithConfiguration:configuration + clientId:clientID + clientSecret:clientSecret + scopes:scopes + redirectURL:[[NSURL alloc] init] + responseType:OIDResponseTypeCode + additionalParameters:additionalParameters]; +} + +#pragma mark - NSObject overrides + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, request: %@>", + NSStringFromClass([self class]), + self, + self.authorizationRequestURL]; +} + +#pragma mark - + +- (NSURLRequest *)URLRequest { + OIDURLQueryComponent *query = [[OIDURLQueryComponent alloc] init]; + + // Required parameters. + [query addParameter:@"client_id" value:self.clientID]; + + if (self.additionalParameters) { + // Add any additional parameters the client has specified. + [query addParameters:(NSDictionary *)self.additionalParameters]; + } + + if (self.scope) { + [query addParameter:@"scope" value:(NSString *)self.scope]; + } + + static NSString *const kHTTPPost = @"POST"; + static NSString *const kHTTPContentTypeHeaderKey = @"Content-Type"; + static NSString *const kHTTPContentTypeHeaderValue = + @"application/x-www-form-urlencoded; charset=UTF-8"; + + GTMTVServiceConfiguration *tvConfiguration = (GTMTVServiceConfiguration *)self.configuration; + + NSMutableURLRequest *URLRequest = + [[NSURLRequest requestWithURL:tvConfiguration.TVAuthorizationEndpoint] mutableCopy]; + URLRequest.HTTPMethod = kHTTPPost; + [URLRequest setValue:kHTTPContentTypeHeaderValue forHTTPHeaderField:kHTTPContentTypeHeaderKey]; + NSString *bodyString = [query URLEncodedParameters]; + NSData *body = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; + URLRequest.HTTPBody = body; + return URLRequest; +} + +@end diff --git a/Source/GTMTVAuthorizationResponse.h b/Source/GTMTVAuthorizationResponse.h new file mode 100644 index 00000000..5f7a76a7 --- /dev/null +++ b/Source/GTMTVAuthorizationResponse.h @@ -0,0 +1,92 @@ +/*! @file GTMTVAuthorizationResponse.h + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import "OIDAuthorizationResponse.h" + +@class GTMTVAuthorizationRequest; +@class OIDTokenRequest; + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief The @c grant_type value for the the TV authorization flow. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +extern NSString *const GTMTVDeviceTokenGrantType; + +/*! @brief Represents the response to a TV authorization request. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +@interface GTMTVAuthorizationResponse : OIDAuthorizationResponse + +/*! @brief The verification URL that should be displayed to the user instructing them to visit the + URL and enter the code. + @remarks verification_url + */ +@property(nonatomic, readonly, nullable) NSString *verificationURL; + +/*! @brief The code that should be displayed to the user which they enter at the @c verificationURL. + @remarks user_code + */ +@property(nonatomic, readonly, nullable) NSString *userCode; + +/*! @brief The device code grant used to poll the token endpoint. Rather than using this directly, + use the provided @c tokenPollRequest method to create the token request. + @remarks device_code + */ +@property(nonatomic, readonly, nullable) NSString *deviceCode; + +/*! @brief The interval at which the token endpoint should be polled with the @c deviceCode. + @remarks interval + */ +@property(nonatomic, readonly, nullable) NSNumber *interval; + +/*! @brief The date at which the user can no longer authorize this request. + @remarks expires_in + */ +@property(nonatomic, readonly, nullable) NSDate *expirationDate; + +/*! @brief Designated initializer. + @param request The serviced request. + @param parameters The decoded parameters returned from the Authorization Server. + @remarks Known parameters are extracted from the @c parameters parameter and the normative + properties are populated. Non-normative parameters are placed in the + @c #additionalParameters dictionary. + */ +- (nullable instancetype)initWithRequest:(GTMTVAuthorizationRequest *)request + parameters:(NSDictionary *> *)parameters + NS_DESIGNATED_INITIALIZER; + +/*! @brief Creates a token request suitable for polling the token endpoint with the @c deviceCode. + @return A @c OIDTokenRequest suitable for polling the token endpoint. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +- (nullable OIDTokenRequest *)tokenPollRequest; + +/*! @brief Creates a token request suitable for polling the token endpoint with the @c deviceCode. + @param additionalParameters Additional parameters for the token request. + @return A @c OIDTokenRequest suitable for polling the token endpoint. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +- (nullable OIDTokenRequest *)tokenPollRequestWithAdditionalParameters: + (nullable NSDictionary *)additionalParameters; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/GTMTVAuthorizationResponse.m b/Source/GTMTVAuthorizationResponse.m new file mode 100644 index 00000000..399bb890 --- /dev/null +++ b/Source/GTMTVAuthorizationResponse.m @@ -0,0 +1,161 @@ +/*! @file GTMTVAuthorizationResponse.m + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "GTMTVAuthorizationResponse.h" + +#import "GTMTVAuthorizationRequest.h" +#import "OIDDefines.h" +#import "OIDError.h" +#import "OIDFieldMapping.h" +#import "OIDTokenRequest.h" + +NSString *const GTMTVDeviceTokenGrantType = @"http://oauth.net/grant_type/device/1.0"; + +/*! @brief The key for the @c verificationURL property in the incoming parameters and for + @c NSSecureCoding. + */ +static NSString *const kVerificationURLKey = @"verification_url"; + +/*! @brief The key for the @c userCode property in the incoming parameters and for + @c NSSecureCoding. + */ +static NSString *const kUserCodeKey = @"user_code"; + +/*! @brief The key for the @c deviceCode property in the incoming parameters and for + @c NSSecureCoding. + */ +static NSString *const kDeviceCodeKey = @"device_code"; + +/*! @brief The key for the @c expirationDate property in the incoming parameters and for + @c NSSecureCoding. + */ +static NSString *const kExpiresInKey = @"expires_in"; + +/*! @brief The key for the @c interval property in the incoming parameters and for + @c NSSecureCoding. + */ +static NSString *const kIntervalKey = @"interval"; + +/*! @brief Key used to encode the @c additionalParameters property for @c NSSecureCoding + */ +static NSString *const kAdditionalParametersKey = @"additionalParameters"; + +/*! @brief Key used to encode the @c request property for @c NSSecureCoding + */ +static NSString *const kRequestKey = @"request"; + +@implementation GTMTVAuthorizationResponse + +@synthesize verificationURL = _verificationURL; +@synthesize userCode = _userCode; +@synthesize deviceCode = _deviceCode; +@synthesize interval = _interval; +@synthesize expirationDate = _expirationDate; + +/*! @brief Returns a mapping of incoming parameters to instance variables. + @return A mapping of incoming parameters to instance variables. + */ ++ (NSDictionary *)fieldMap { + static NSMutableDictionary *fieldMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + fieldMap = [NSMutableDictionary dictionary]; + fieldMap[kVerificationURLKey] = + [[OIDFieldMapping alloc] initWithName:@"_verificationURL" type:[NSString class]]; + fieldMap[kUserCodeKey] = + [[OIDFieldMapping alloc] initWithName:@"_userCode" type:[NSString class]]; + fieldMap[kDeviceCodeKey] = + [[OIDFieldMapping alloc] initWithName:@"_deviceCode" type:[NSString class]]; + fieldMap[kExpiresInKey] = + [[OIDFieldMapping alloc] initWithName:@"_expirationDate" + type:[NSDate class] + conversion:^id _Nullable(NSObject *_Nullable value) { + if (![value isKindOfClass:[NSNumber class]]) { + return value; + } + NSNumber *valueAsNumber = (NSNumber *)value; + return [NSDate dateWithTimeIntervalSinceNow:[valueAsNumber longLongValue]]; + }]; + fieldMap[kIntervalKey] = + [[OIDFieldMapping alloc] initWithName:@"_interval" type:[NSNumber class]]; + }); + return fieldMap; +} + +#pragma mark - Initializers + +- (nullable instancetype)init + OID_UNAVAILABLE_USE_INITIALIZER(@selector(initWithRequest:parameters:)); + +- (nullable instancetype)initWithRequest:(GTMTVAuthorizationRequest *)request + parameters:(NSDictionary *> *)parameters { + self = [super initWithRequest:request parameters:parameters]; + return self; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + // The documentation for NSCopying specifically advises us to return a reference to the original + // instance in the case where instances are immutable (as ours is): + // "Implement NSCopying by retaining the original instead of creating a new copy when the class + // and its contents are immutable." + return self; +} + +#pragma mark - NSObject overrides + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, verificationURL: %@, userCode: \"%@\", deviceCode: " + "\"%@\", interval: %@, expirationDate: %@, " + "additionalParameters: %@, " + "request: %@>", + NSStringFromClass([self class]), + self, + _verificationURL, + _userCode, + _deviceCode, + _interval, + _expirationDate, + self.additionalParameters, + self.request]; +} + +#pragma mark - + +- (OIDTokenRequest *)tokenPollRequest { + return [self tokenPollRequestWithAdditionalParameters:nil]; +} + +- (OIDTokenRequest *)tokenPollRequestWithAdditionalParameters: + (NSDictionary *)additionalParameters { + OIDTokenRequest *pollRequest = + [[OIDTokenRequest alloc] initWithConfiguration:self.request.configuration + grantType:GTMTVDeviceTokenGrantType + authorizationCode:_deviceCode + redirectURL:[[NSURL alloc] init] + clientID:self.request.clientID + clientSecret:self.request.clientSecret + scopes:nil + refreshToken:nil + codeVerifier:nil + additionalParameters:nil]; + return pollRequest; +} + +@end diff --git a/Source/GTMTVAuthorizationService.h b/Source/GTMTVAuthorizationService.h new file mode 100644 index 00000000..dfba4ba9 --- /dev/null +++ b/Source/GTMTVAuthorizationService.h @@ -0,0 +1,82 @@ +/*! @file GTMTVAuthorizationService.h + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class GTMAppAuthFetcherAuthorization; +@class GTMTVAuthorizationRequest; +@class GTMTVAuthorizationResponse; +@class GTMTVServiceConfiguration; + +/*! @brief The block that is called when the TV authorization has initialized. + @param response The authorization response, or nil if there was an error. Display + @c GTMTVAuthorizationResponse.userCode and @c GTMTVAuthorizationResponse.verificationURL to + the user so they can action the request. + @param error The error if an error occurred. + */ +typedef void (^GTMTVAuthorizationInitialization)(GTMTVAuthorizationResponse *_Nullable response, + NSError *_Nullable error); + +/*! @brief The block that is called when the TV authorization has completed. + @param authorization The @c GTMAppAuthFetcherAuthorization which you can use to authorize + API calls, or nil if there was an error. + @param error The error if an error occurred. + */ +typedef void (^GTMTVAuthorizationCompletion) + (GTMAppAuthFetcherAuthorization *_Nullable authorization, + NSError *_Nullable error); + +/*! @brief Block returned when authorization is initialized to that will cancel the pending + authorization when executed. Has no effect if called twice or after the authorization + concluded. + */ +typedef void (^GTMTVAuthorizationCancelBlock)(); + +/*! @brief Performs authorization flows designed for TVs and other limited input devices. + */ +@interface GTMTVAuthorizationService : NSObject + +#if !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT +/*! @brief Convenience method to return the TV authorization URL for Google. + @return TV authorization URL for Google. + */ ++ (GTMTVServiceConfiguration *)TVConfigurationForGoogle; +#endif // !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT + +/*! @brief Starts a TV authorization flow with the given request and polls for a response. + @param request The TV authorization request to initiate. + @param initialization Block that is called with the initial authorization response. Unlike other + OAuth authorization responses, the TV authorization response doesn't contain the + authorization as the user has yet to grant it. Rather, it contains the information that you + show to the user in order for them to authorize the request on another device. + @param completion Block that is called on the success or failure of the authorization. If the + user approves the request, you will get a @c GTMAppAuthFetherAuthorization that you can use + to authenticate API calls, otherwis eyou will get an error. + @return A block which you can execute if you need to cancel the ongoing authorization. Has no + effect if called twice, or called after the authorization concludes. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ ++ (GTMTVAuthorizationCancelBlock)authorizeTVRequest:(GTMTVAuthorizationRequest *)request + initializaiton:(GTMTVAuthorizationInitialization)initialization + completion:(GTMTVAuthorizationCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/GTMTVAuthorizationService.m b/Source/GTMTVAuthorizationService.m new file mode 100644 index 00000000..d91804c6 --- /dev/null +++ b/Source/GTMTVAuthorizationService.m @@ -0,0 +1,254 @@ +/*! @file GTMAppAuthFetcherAuthorization.m + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "GTMTVAuthorizationService.h" + +#import "OIDAuthState.h" +#import "OIDAuthorizationService.h" +#import "OIDDefines.h" +#import "OIDErrorUtilities.h" +#import "OIDURLQueryComponent.h" +#import "GTMAppAuthFetcherAuthorization.h" +#import "GTMTVAuthorizationRequest.h" +#import "GTMTVAuthorizationResponse.h" +#import "GTMTVServiceConfiguration.h" + +/*! @brief Google's device authorization endpoint. + */ +NSString *const kGoogleDeviceAuthorizationEndpoint = + @"https://accounts.google.com/o/oauth2/device/code"; + +/*! @brief The authorization pending error code. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +NSString *const kErrorCodeAuthorizationPending = @"authorization_pending"; + +/*! @brief The slow down error code. + @see https://developers.google.com/identity/protocols/OAuth2ForDevices + */ +NSString *const kErrorCodeSlowDown = @"slow_down"; + +@implementation GTMTVAuthorizationService + +#pragma mark - Initializers + +#if !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT ++ (GTMTVServiceConfiguration *)TVConfigurationForGoogle { + NSURL *authorizationEndpoint = + [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"]; + NSURL *tokenEndpoint = + [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"]; + NSURL *TVAuthorizationEndpoint = + [NSURL URLWithString:kGoogleDeviceAuthorizationEndpoint]; + + GTMTVServiceConfiguration *configuration = + [[GTMTVServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint + TVAuthorizationEndpoint:TVAuthorizationEndpoint + tokenEndpoint:tokenEndpoint]; + return configuration; +} +#endif // !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT + ++ (GTMTVAuthorizationCancelBlock)authorizeTVRequest:(GTMTVAuthorizationRequest *)request + initializaiton:(GTMTVAuthorizationInitialization)initialization + completion:(GTMTVAuthorizationCompletion)completion { + // Block level variable that can be used to cancel the polling. + __block BOOL pollRunning = YES; + + // Block that will be returned allowign the caller to cancel the polling. + GTMTVAuthorizationCancelBlock cancelBlock = ^{ + if (pollRunning) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *cancelError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeProgramCanceledAuthorizationFlow + underlyingError:nil + description:@"Authorization cancelled"]; + completion(nil, cancelError); + }); + } + pollRunning = NO; + }; + + // Performs the initial authorization reqeust. + NSURLRequest *URLRequest = [request URLRequest]; + NSURLSession *session = [NSURLSession sharedSession]; + [[session dataTaskWithRequest:URLRequest + completionHandler:^(NSData *_Nullable data, + NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + // A network error or server error occurred. + NSError *returnedError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeNetworkError + underlyingError:error + description:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + initialization(nil, returnedError); + }); + return; + } + + NSHTTPURLResponse *HTTPURLResponse = (NSHTTPURLResponse *)response; + + if (HTTPURLResponse.statusCode != 200) { + // A server error occurred. + NSError *serverError = + [OIDErrorUtilities HTTPErrorWithHTTPResponse:HTTPURLResponse data:data]; + + // HTTP 400 may indicate an RFC6749 Section 5.2 error response, checks for that + if (HTTPURLResponse.statusCode == 400) { + NSError *jsonDeserializationError; + NSDictionary *> *json = + [NSJSONSerialization JSONObjectWithData:(NSData *)data + options:0 + error:&jsonDeserializationError]; + + // if the HTTP 400 response parses as JSON and has an 'error' key, it's an OAuth error + // these errors are special as they indicate a problem with the authorization grant + if (json[OIDOAuthErrorFieldError]) { + NSError *oauthError = + [OIDErrorUtilities OAuthErrorWithDomain:OIDOAuthTokenErrorDomain + OAuthResponse:json + underlyingError:serverError]; + dispatch_async(dispatch_get_main_queue(), ^{ + initialization(nil, oauthError); + }); + return; + } + } + + // not an OAuth error, just a generic server error + NSError *returnedError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeServerError + underlyingError:serverError + description:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + initialization(nil, returnedError); + }); + return; + } + + NSError *jsonDeserializationError; + NSDictionary *> *json = + [NSJSONSerialization JSONObjectWithData:(NSData *)data + options:0 + error:&jsonDeserializationError]; + if (jsonDeserializationError) { + // A problem occurred deserializing the response/JSON. + NSError *returnedError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeJSONDeserializationError + underlyingError:jsonDeserializationError + description:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + initialization(nil, returnedError); + }); + return; + } + + // Parses the authorization response. + GTMTVAuthorizationResponse *TVAuthorizationResponse = + [[GTMTVAuthorizationResponse alloc] initWithRequest:request parameters:json]; + if (!TVAuthorizationResponse) { + // A problem occurred constructing the token response from the JSON. + NSError *returnedError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeTokenResponseConstructionError + underlyingError:jsonDeserializationError + description:nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + initialization(nil, returnedError); + }); + return; + } + + // Calls the initialization block to signal that we received a TV authorization response. + dispatch_async(dispatch_get_main_queue(), ^() { + initialization(TVAuthorizationResponse, nil); + }); + + // Creates the token request that will be used to poll the token endpoint. + OIDTokenRequest *pollRequest = [TVAuthorizationResponse tokenPollRequest]; + + // Starting polling interval (may be increased if a slow down message is received). + __block NSTimeInterval interval = [TVAuthorizationResponse.interval doubleValue]; + + // Polls the token endpoint until the authorization completes or expires. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + do { + // Sleeps for polling interval. + [NSThread sleepForTimeInterval:interval]; + + if (!pollRunning) { + break; + } + + // Polls token endpoint. + [OIDAuthorizationService performTokenRequest:pollRequest + callback:^(OIDTokenResponse *_Nullable tokenResponse, + NSError *_Nullable tokenError) { + if (!pollRunning) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^() { + if (tokenResponse) { + // Success response. + pollRunning = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + OIDAuthState *authState = + [[OIDAuthState alloc] initWithAuthorizationResponse:TVAuthorizationResponse + tokenResponse:tokenResponse]; + GTMAppAuthFetcherAuthorization *authorization = + [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; + completion(authorization, nil); + }); + } else { + if (tokenError.domain == OIDOAuthTokenErrorDomain) { + // OAuth token errors inspected for device flow specific errors. + NSString *errorCode = + tokenError.userInfo[OIDOAuthErrorResponseErrorKey][OIDOAuthErrorFieldError]; + if ([errorCode isEqual:kErrorCodeAuthorizationPending]) { + // authorization_pending is an expected response. + return; + } else if ([errorCode isEqual:kErrorCodeSlowDown]) { + // Increase interval by 20%, enforce a lower bound of 5s. + interval *= 1.20; + interval = MAX(5.0, interval); + } else { + // Unhandled token error, considered fatal. + pollRunning = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, tokenError); + }); + } + } else { + // All other errors considered fatal. + pollRunning = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, tokenError); + }); + } + } + }); + }]; + } while ([TVAuthorizationResponse.expirationDate timeIntervalSinceNow] > 0 && pollRunning); + }); + }] resume]; + + return cancelBlock; +} + +@end diff --git a/Source/GTMTVServiceConfiguration.h b/Source/GTMTVServiceConfiguration.h new file mode 100644 index 00000000..85eb9b0f --- /dev/null +++ b/Source/GTMTVServiceConfiguration.h @@ -0,0 +1,61 @@ +/*! @file GTMTVServiceConfiguration.h + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "OIDServiceConfiguration.h" + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief Configuration for authorizing the user with the @c GTMTVAuthorizationService. + */ +@interface GTMTVServiceConfiguration : OIDServiceConfiguration + +/*! @brief The TV authorization endpoint URI. + */ +@property(nonatomic, readonly) NSURL *TVAuthorizationEndpoint; + +/*! @internal + @brief Unavailable. Please use + @c initWithAuthorizationEndpoint:TVAuthorizationEndpoint:tokenEndpoint: + */ +- (instancetype)init NS_UNAVAILABLE; + +/*! @internal + @brief Unavailable. Please use + @c initWithAuthorizationEndpoint:TVAuthorizationEndpoint:tokenEndpoint: + */ +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint NS_UNAVAILABLE; + +/*! @internal + @brief Unavailable. Please use + @c initWithAuthorizationEndpoint:TVAuthorizationEndpoint:tokenEndpoint: + */ +- (instancetype)initWithDiscoveryDocument:(OIDServiceDiscovery *)discoveryDocument NS_UNAVAILABLE; + +/*! @brief Designated initializer. + @param authorizationEndpoint The authorization endpoint URI. + @param TVAuthorizationEndpoint The TV authorization endpoint URI. + @param tokenEndpoint The token exchange and refresh endpoint URI. + */ +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + TVAuthorizationEndpoint:(NSURL *)TVAuthorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/GTMTVServiceConfiguration.m b/Source/GTMTVServiceConfiguration.m new file mode 100644 index 00000000..2f8937ab --- /dev/null +++ b/Source/GTMTVServiceConfiguration.m @@ -0,0 +1,96 @@ +/*! @file GTMTVServiceConfiguration.m + @brief GTMAppAuth SDK + @copyright + Copyright 2016 Google Inc. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "GTMTVServiceConfiguration.h" + +#import "OIDDefines.h" +#import "OIDErrorUtilities.h" +#import "OIDServiceDiscovery.h" + +/*! @brief The key for the @c TVAuthorizationEndpoint property. + */ +static NSString *const kTVAuthorizationEndpointKey = @"TVAuthorizationEndpoint"; + +NS_ASSUME_NONNULL_BEGIN + +@interface GTMTVServiceConfiguration () + +/*! @brief Designated initializer. + @param aDecoder NSCoder to unserialize the object from. + */ +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; + +@end + +@implementation GTMTVServiceConfiguration + +@synthesize TVAuthorizationEndpoint = _TVAuthorizationEndpoint; + +- (instancetype)init + OID_UNAVAILABLE_USE_INITIALIZER( + @selector(initWithAuthorizationEndpoint:TVAuthorizationEndpoint:tokenEndpoint:)); + +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint + OID_UNAVAILABLE_USE_INITIALIZER( + @selector(initWithAuthorizationEndpoint:TVAuthorizationEndpoint:tokenEndpoint:)); + +- (instancetype)initWithAuthorizationEndpoint:(NSURL *)authorizationEndpoint + TVAuthorizationEndpoint:(NSURL *)TVAuthorizationEndpoint + tokenEndpoint:(NSURL *)tokenEndpoint { + self = [super initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint]; + if (self) { + _TVAuthorizationEndpoint = [TVAuthorizationEndpoint copy]; + } + return self; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + NSURL *TVAuthorizationEndpoint = [aDecoder decodeObjectOfClass:[NSURL class] + forKey:kTVAuthorizationEndpointKey]; + _TVAuthorizationEndpoint = TVAuthorizationEndpoint; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [super encodeWithCoder:aCoder]; + [aCoder encodeObject:_TVAuthorizationEndpoint forKey:kTVAuthorizationEndpointKey]; +} + +#pragma mark - description + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, TVAuthorizationEndpoint: %@ tokenEndpoint: %@>", + NSStringFromClass([self class]), + self, + _TVAuthorizationEndpoint, + self.tokenEndpoint]; +} + +@end + +NS_ASSUME_NONNULL_END